Loading lang_cpp_04_struct...

enib_small.png LANG-CPP 04_Struct — Types structurés

Nous avons déjà rencontré et utilisé, au cours des chapitres précédents, des types élaborés (std::string, std::vector<T>...).
Ils nous offrent, à travers des fonctions membres ou non, une variété de services tout en garantissant leur propre intégrité (concernant la gestion de leurs ressources internes notamment).
Nous avons à plusieurs reprises prêté attention au fait que des données de tels types étaient nécessairement initialisées, que ce soit explicitement ou implicitement, et que leur transmission pouvait donner lieu à des opérations non triviales et à des restrictions (recopie, déplacement).
Comme nous l'évoquions en introduction, la robustesse de la réalisation de programmes en langage C++ repose sur la définition de types, qui viennent étendre les types de base, et qui imposent strictement les opérations qu'il est légitime de leur appliquer.
Ce chapitre vise alors à définir de tels types par nos propres moyens.
{1 #init } Initialisation
{2 #invariant } Maintien d'un invariant
{3 #disable_copy } Inhibition de la recopie
{4 #enable_copy } Réalisation explicite de la recopie
{5 #special_members } Les fonctions membres spéciales

Ce chapitre apporte enfin quelques éléments qui aident à comprendre comment les types élaborés (comme std::string ou std::vector<T> par exemple) garantissent le maintien de leur intégrité et l'assurance de leurs services à travers les multiples circonstances applicatives dans lesquelles ils sont employés.

La notion de constructeur est fondamentale puisqu'elle garantit qu'aucune de nos données ne puisse être créée sans que son état ne soit précisément déterminé, notamment grâce à la liste d'initialisation des membres.
Un même type peut offrir plusieurs constructeurs, qui peuvent éventuellement s'invoquer mutuellement par délégation, avec des signatures variées répondant à différents besoins applicatifs ; le constructeur par défaut n'est que le cas particulier d'un constructeur n'attendant aucun paramètre.

Les types structurés qui fournissent des services un tant soit peu élaborés ont généralement besoin de maintenir des invariants qui lient leurs données membres.
L'utilisation de modificateurs d'accès permet d'envisager la démarche d'encapsulation.
Il s'agit de rendre inaccessibles au reste de l'application les ressources à usage privé de ce type, pour ne laisser apparaître que les fonctionnalités publiques.
Seules les fonctions membres du type concerné sont autorisées à accéder aux membres privés.
Cette démarche tend à découpler les détails de réalisation d'un type structuré vis-à-vis du contexte applicatif qui l'utilise ; en ne reposant que sur un ensemble minimal de fonctions membres, nous favorisons une bonne maintenabilité et une bonne réutilisabilité du type structuré en question.

Une fonction membre s'applique à une donnée particulière dont l'adresse nous est rendue accessible avec le mot-clef this (un pointeur sur le type en question).
Si une fonction membre est qualifiée de const, alors elle est applicable à une donnée considérée comme constante : il faut systématiquement utiliser ce qualificatif lorsque la fonction membre ne sert qu'à consulter l'état de la donnée concernée.
Les fonctions membres qui ne sont pas qualifiées de const servent à modifier l'état de la donnée concernée ; elles doivent veiller à respecter les invariants inhérents aux services rendus par ce type structuré.

Il existe cinq fonctions membres spéciales :
Elle sont spéciales dans le sens où, lorsque nous ne les déclarons pas explicitement, le compilateur peut en fournir des versions implicitement générées qui se contentent d'appliquer le traitement équivalent à chacune des données membres.
Il est recommandé, par ordre de préférence :
  1. de se contenter de ces versions implicitement générées lorsqu'elles sont suffisantes (ce qui répond parfaitement à la plupart des besoins courants dans la pratique),
    • il suffit de ne déclarer aucune de ces cinq fonctions membres spéciales,
    • il est également envisageable de les déclarer toutes les cinq avec la notation =default afin de signifier très clairement que nous assumons l'utilisation de ces versions automatiquement générées,
  2. de déclarer chacune des cinq avec la notation =default ou =delete selon les cas afin d'interdire les quelques versions automatiquement générées qui ne seraient pas adaptées à nos exigences,
  3. de n'envisager une définition explicite de ces fonctions membres spéciales qu'en tout dernier recours et sous le contrôle d'un programmeur très expérimenté,
    • c'est très rarement nécessaire en pratique, il faut alors s'imposer de les définir toutes les cinq (que ce soit avec =default, =delete ou explicitement).

La découverte de ces mécanismes, de ces principes et de ces recommandations doit désormais nous guider dans la réalisation de nos propres types structurés afin qu'ils soient fiables et efficaces.

Code source de l'expérimentation