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.

Schématiquement, résoudre un problème informatique peut se décomposer en deux parties :

  • définir les données utilisées et leur organisation (les structures de données) ;
  • définir les traitements sur ces données (les algorithmes).

Créer des types en C++ concernent ces deux aspects d'un problème.

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.

Définir un type

Il existe différentes façons de définir des structures de données.

  • 1 élément de type hétérogène = struct
  • plusieurs éléments de même type = collection
  • autres…

Alias de type

= 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.

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.

Type et sémantique

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 :

  • 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.1450148095.txt.gz · Dernière modification: 2015/12/15 03:54 par gbdivers