Ceci est une ancienne révision du document !
structure
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.
Schématiquement, résoudre un problème informatique peut se décomposer en deux parties :
Créer des types en C++ concernent ces deux aspects d'un problème.
Les types que vous définirez seront utilisé à deux niveaux :
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.
Il existe différentes façons de définir des structures de données.
= donner un nouveau nom à un type.
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.
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).
= mettre plusieurs types/variable ensemble.
struct NOM { ... };
version simple = simple composition de données, sans sémantique détaillée.
Pouvoir définir un type présente une autre utilité. Lorsque l'on utilise un type (par exemple int
), ce type a un sens en termes de représentation (int
représente un nombre entier positif ou négatif, sur 32 bits), mais il n'a aucun sens en termes de sémantique. C'est à dire qu'ils ne signifient rien pour celui qui lit le code, en dehors du sens donné par le langage.
Par exemple, dans le code suivant, un entier peut correspondre à un nombre de minutes ou à un nombre d'heures, cela ne changerait rien pour le compilateur. Par contre, cela a une importance pour celui qui écrit le code, mais cette information n’apparaît par directement dans le code.
int temps_1 { 12 }; // heure int temps_2 { 40 }; // minutes
On peut, comme dans le code précédent, indiquer cette information en commentaire, mais rien ne permet de garantir que la sémantique sera respectée. Ainsi, on peut additionner ces deux nombres, sans que cela ne provoque le moindre problème pour le compilateur. Alors qu'additionner des heures et des minutes n'a aucun sens pour nous.
int temps_total { temps_1 + temps_2 };
Une première approche pour corriger ce problème pourrait être de renommer explicitement les types, pour leur donner un nom significatif. Par exemple, nous pouvez créer les types heure
et minute
de la façon suivante :
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 les plus faciles à écrire (en général, pour gagner un maximum de temps lors de l'écriture), 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.
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 :
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).