Outils d'utilisateurs

Outils du Site


definir_ses_types

Ceci est une ancienne révision du document !


structure

Créer des nouveaux types

Intérêt et importance de définir ses types

Le C++ fournit de nombreux types fondamentaux, pour les besoins de base (et vous ne les avez pas encore tous vu). Les types sont des informations fondamentales pour le compilateur, pour générer un programme exécutable, mais également pour réaliser des optimisations et vérifier leur utilisation correcte (vous avez vu par exemple la vérification des dépassements de valeur - le narrowing).

Il est tout à fait possible d'écrire des programmes utilisant uniquement ces types fondamentaux, mais le C++ va plus loin et permet de définir ses propres types, plus ou moins complexes, en combinant des types fondamentaux et d'autres types complexes.

Les types que vous définirez seront utilisé à deux niveaux :

  • par le compilateur, pour connaître la structure en mémoire (représentation binaire), les optimisations possibles, etc.
  • par les “utilisateurs” de vos types (les autres développeurs, qui utiliseront votre code, pas les utilisateurs de votre programme), pour savoir comment les utiliser correctement (syntaxe) et ce qu'ils représentent (sémantique).

Avant de chercher à obtenir les meilleurs performances possibles, la première chose à faire sera toujours de définir correctement ses types en termes de syntaxe et de sémantique.

L'une des forces du C++ est de permet de définir des types parfois très complexes, avec de nombreuses contraintes, mais qui seront validés lors de la phase de compilation. Ce qui signifie que quelque soit la complexité de vos types, cela n'aura pas d'impact sur les performances.

On dit parfois que le C++ est un langage performant. Ce n'est pas tout à fait vrai : le C++ est avant tout un langage qui vous donne le contrôle de ces performances. Mais cela nécessite d'écrire du code correcte et demande souvent du travail supplémentaire pour obtenir ces performances optimales.

Alias de type

La première façon de définir un nouveau type est de créer un alias de type. Cela consiste simplement à donner un nouveau nom à un type. L'intérêt est de pouvoir donner un nom court à un type complexe, de donner un nom plus expressif à un type ou de faciliter l'évolution du code.

La syntaxe utilise le mot-clé using :

using NOM = TYPE;

NOM correspond au nouveau type que vous souhaitez créer et TYPE au type correspondant. Plus concrètement, vous pouvez écrire par exemple :

using mon_type_entier = int;

Nom court

Un alias de type peut être utilisé pour simplifier le code, en donnant un nom court à des types. Par exemple :

using ull_int = unsigned long long int;

Cette technique est particulièrement intéressante avec les types complexes, par exemple ceux que l'on trouve dans la bibliothèque standard, et avec la méta-programmation. Par exemple, les types à taille fixe int8_t, int16_t, etc. sont des alias de type.

// "Exact-width integer types" définis dans MingW 4.9.2
usgin int8_t  = signed char;
using int16_t = short;

Nom plus expressif

Les noms des types apportent des informations sur leur représentation, ce qu'ils signifient. Par exemple, vous savez que si vous voyez le type int, vous savez que cela représente un entier signé, généralement codé sur 32 bits, avec une valeur maximale et minimale que vous pouvez obtenir avec std::std::numeric_limits, etc.

Par contre, vous ne savez pas forcement ce que représente une variable de ce type. Cette information peut être apportée par le nom des variables, mais il est également possible d'utiliser les alias de type pour renforcement l'expressivité des types.

Par exemple, si vous créez deux variables représentant des temps en heures et en minutes :

int temps_1 { 12 }; // heure
int temps_2 { 40 }; // minutes

Les commentaires ne sont pas suffisants, ils seront vite oublié plus loin dans le code. Un alias de type permet d'avoir des noms plus explicites. Par exemple :

using heure = int;
using minute = int;
 
heure  temps_1 { 12 };
minute temps_2 { 40 };

Cette correction peut sembler mineure, mais elle correspond à un principe important en programmation. Un code est plus souvent lu qu'il n'est écrit. Il ne faut donc pas écrire vos codes pour qu'ils soient plus faciles à écrire, mais pour qu'ils soient les plus faciles à lire. L'expérience montre que l'on perd beaucoup de temps à comprendre un ancien code pour corriger les erreurs de programmation ou le faire évoluer.

typedef

compilateur traite de la même facon

Dans les chapitres précédents, vous avez appris qu'il était possible de modifier des types en ajoutant des modificateurs. Il sera par exemple possible de passer d'un entier sur 32 bits (int) en un entier sur 16 bits en ajoutant le mot-clé short.

Imaginez que vous écrivez le code suivant :

int x {};
int y {};
int z {};
int a {};
int b {};
int c {};

Après analyse des contraintes imposées par les données, il apparaît que les valeurs prises par ces variables seront comprises entre 0 et 100. Vous souhaitez donc changer les types pour utiliser un type entier non signé sur 8 bits std::uint8_t au lieu d'un type entier signé int. Vous faites donc le remplacement de tous les types des variables :

std::uint8_t x {};
std::uint8_t y {};
std::uint8_t z {};
std::uint8_t a {};
std::uint8_t b {};
std::uint8_t c {};

On voit tout de suite le problème, puisque c'est le même qui a été présenté pour les variables. La modification manuelle de plusieurs lignes de code diminue l'évolutivité du code et donc la qualité logicielle du programme.

Pour corriger ce problème, on va pouvoir utiliser une solution équivalente à celle utilisée pour les variables. On va pouvoir créer un nouveau type, qui sera en fait un type créé à partir d'un type de base et de modificateurs et auquel on donnera un nouveau nom.

La syntaxe est la suivante, en utilisant le mot-clé using :

using local_type = int;
local_type x {};
local_type y {};
local_type z {};
local_type a {};
local_type b {};
local_type c {};

Avec ce code, il ne sera plus nécessaire de changer plusieurs lignes si on souhaite changer le type des variables. Le code a gagné en maintenabilité et en qualité logicielle.

Attention, vous avez déjà rencontré using, pour déclarer l'utilisation d'un espace de noms. Même si on utilise le même mot-clé, il s'agit bien de deux utilisation différentes et donc deux syntaxes différentes. Pour rappel :

using namespace std; // déclaration d'un espace de noms
using std::cout; // déclaration d'une fonctionnalité d'un espace de noms
 
using nouveau_type = ancien_type; // déclaration d'un nouveau type

Pour les noms de type, les règles sont les mêmes que pour les noms de variables (caractères alphanumériques et tiret bas et doit commencer par une lettre). De la même façon, il est classique de définir des règles de codage pour préciser comment il faut écrire les noms de type. Par exemple, il sera classique de terminer un nom de type avec “_t” ou “_type” (par exemple avec la bibliothèque standard ou la bibliothèque Boost) ou avec une majuscule (par exemple avec la bibliothèque Qt).

Struct

= mettre plusieurs types/variable ensemble.

struct NOM {
    ...
};

version simple = simple composition de données, sans sémantique détaillée.

limitations

Il faut donc écrire un code en gardant en tête la question “comment pourrais-je réutiliser ce code ?” (ré-utilisabilité). Il faut alors respecter les règles suivantes :

  • Présentez correctement vos codes. Passez à la ligne, indentez vos lignes (c'est-à-dire ajoutez des espaces en début de ligne pour que vos codes soient alignés), aérez votre code (utilisez des espaces pour faciliter la lecture) ;
  • Regroupez vos codes en termes de fonctionnalités. Vous verrez par la suite comment regrouper vos codes dans des fichiers pour créer des fonctions, classes et modules.
  • Donnez un sens aux choses. Donnez des noms de variable et de type qui ont un sens (sémantique).

Donner un nom explicite aux types vise à répondre à ce troisième point. Cependant, cette approche ne sera pas suffisante. Donner un sens aux choses est intéressant, faire que le compilateur comprennent le sens donné et valide leur bonne utilisation, c'est encore mieux. Ainsi, même en créant des types heure et minute, le compilateur ne fait aucune validation (pour lui, ce sont des entiers) et ne posera pas de problèmes à les additionner.

??? temps_total { temps_1 + temps_2 }; // Quel est le type résultant ?

Il faudrait au minimum que le compilateur prévienne que l'on essaye d'additionner deux concepts distincts. Ou encore mieux, qu'il comprennent que 1 heure = 60 minutes et qu'il fasse l'addition en faisant la conversion, pour donner le résultat correct. La sémantique des types est incomplète en créant de nouveaux types de cette façon.

Mais heureusement, le C++ propose des techniques plus avancées pour proposer une sémantique complète, validée par le compilateur. Vous verrez dans la suite du cours comment créer ce type de sémantique, en utilisant des classes (programmation orientée objet) et la méta-programmation (création de langages spécifiques d'un domaine - DSL).

definir_ses_types.1450255491.txt.gz · Dernière modification: 2015/12/16 09:44 par gbdivers