Ce qui suit présume quelques connaissances de programmation « système », donc de bas niveau.
La langage C++ a des qualités et des défauts, sans surprises. L'une de ses grandes qualités, du moins à mes yeux, est qu'il permet de définir des abstractions à coût nul, et qu'il permet de choisir les moments et les raisons pour lesquelles une programmeuse ou un programmeur choisira de payer un prix, en espace ou en temps d'exécution, pour un mécanisme ou une fonctionnalité. C'est ce grand contrôle qu'il offre aux programmeuses et aux programmeurs qui font, en grande partie, qu'on l'apprécie tant pour certains types de programmation.
Selon le standard (du moins selon http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3797.pdf, §9), certaines caractéristiques sont particulièrement intéressantes à connaître pour des types destinés à des tâches que l'on associerait à de la « programmation système ».
Un agrégat est une bestiole particulière dans le monde de C++. Sur la base du standard (C++ 03), on voit [dcl.init.aggr] :
An aggregate is an array or a class (§9) with no user-declared constructors (§12.1), no private or protected non-static data members (§11), no base classes (§10), and no virtual functions (§10.3).
Un agrégat est un simple assemblage d'états, eux-mêmes triviaux ou agrégats, et publics. Ceci modélise bien les types du langage C, à ceci près qu'un agrégat de C++ peut avoir des méthodes d'instance non-polymorphiques. Un agrégat peut avoir des membres de classe (qualifiés static) puisque cela n'affecte en rien sa constitution interne.
Quelques exemples :
Agrégats | Non-agrégats |
---|---|
|
|
Les règles propres aux agrégats ont évolué avec l'avènement de C++ 11. Nous débuterons en expliquant ce qui prévaut depuis (au moins) C++ 03, puis nous examinerons ce qui prévaut depuis C++ 11 pour compléter le portrait (vous pouvez y aller directement si vous le souhaitez). Nous examinerons ensuite ce qui prévaut depuis C++ 14 (vous pouvez y aller directement si vous le souhaitez).
Un agrégat peut être initialisé directement et implictement à l'aide de valeurs placées entre accolades, même avec un compilateur C++ 03, et il en va de même pour un tableau d'agrégats comme le montre l'exemple à droite. La pratique d'initaliser les membres d'un agrégat en ordre de déclaration, par une séquence de valeurs, est ce qu'on nomme l'aggregate initialization, détaillée dans http://en.cppreference.com/w/cpp/language/aggregate_initialization Dans un cas comme celui de ptsInc, où le tableau n'est pas entièrement initialisé, les éléments résiduels (ici, l'élément à la position 4) seront value-initialized si cela s'avère possible. La value-initialization s'applique d'ailleurs à tous les agrégats initialisés de manière incomplète, donc à pt.y dans l'exemple à droite. |
|
Le cas des union agrégats est particulier, en ceci qu'on ne peut initialiser que leur premier membre à l'aide d'accolades; ainsi, si vous avez recours à un union, prenez soin de disposer les attributs à l'intérieur de manière à ce que cette disposition respecte votre intention. Ces modalités particulières pour l'initialisation des agrégats tiennent au fait qu'un agrégat se veut, structurellement, une séquence d'états, sans plus. Si nous ajoutons un constructeur « maison », cela envoie le signal que l'initialisation de l'agrégat n'est pas triviale, et que l'initialisation reposant simplement sur des valeurs placées entre accolades serait insuffisante pour porter l'intention des programmeuses ou des programmeurs. |
|
Les POD (ci-dessous) sont des agrégats, mais soumis à des contraintes plus rigides encore. Techniquement, un POD est un agrégat pour lequel la Sainte-Trinité est implicite, donc laissée aux soins du compilateur (=default ou simplement non-déclarée). De plus, tout attribut d'instance d'un POD ne peut pas être lui-même non-POD, un tableau de non-POD, ou une référence. Les POD sont pratiquement des struct du langage C, mais peuvent avoir des membres de classe et des méthodes non-polymorphiques.
.Pourquoi se préoccuper des POD? Il se trouve que ces types ont un certain nombre de caractéristiques intéressantes, parmi lesquelles on trouve (liste non-exhaustive) :
L'avènement de C++ 11 apporte quelques raffinements au concept d'agrégat. Par exemple :
Pour le reste, l'essence de ce qu'est un agrégat n'a pas changé de manière fondamentale.
Le texte descriptif des POD a beaucoup évolué, mais le fond demeure : un POD décrit une entité susceptible d'être (a) initialisée de manière statique, et (b) disposée en mémoire de manière compatible avec les usages du code C.
Ces deux concepts sont distincts, ce qui explique qu'on parle désormais de classes triviales et de classes à disposition standard. Chacun de ces concepts a une utilité propre à lui et peut rendre de précieux services sans dépendre de l'autre. Un POD devient donc essentiellement une classe à la fois triviale et de disposition standard, et dont les membres d'instance et les parents respectent aussi (récursivement) ces contraintes; ces nouveaux concepts sont ceux par lesquels le standard s'exprime désormais.
Une classe est « triviale » si :
En particulier, une classe « triviale » n'a ni méthode virtuelle, ni ancêtre virtuel. On vise ici une classe qui se rapproche, structurellement, d'un agrégat de données, comme le sont des struct du langage C. Notez que ceci ne signifie pas qu'une classe triviale ne peut avoir de méthodes, mais elle ne peut avoir de méthodes virtuelles – cela imposerait au compilateur de lui ajouter une vtbl pour entreposer les pointeurs sur ses méthodes, et la rendrait non-triviale. La restriction sur l'héritage virtuel est d'ailleurs de même nature.
Il n'y a pas de restrictions quant aux qualifications d'accès pour une classe triviale. On peut donc alterner les spécifications privé, protégé et public à loisir. En ceci, on peut être trivial sans être de disposition standard.
Les opérations clés pour déterminer la trivialité ou non d'une classe sont celles qui contrôlent la vie des objets, soit celles qui en régissent laconstruction, la destruction, la copie et le mouvement. Ainsi, une opération de copie ou de mouvement est « triviale » si :
La trivialité est, manifestement, virale, puisqu'elle modélise un objet si banal que le compilateur peut pleinement le prendre en charge.
Le concept de « trivialement copiable » est intéressant du fait qu'il permet au compilateur de remplacer des opérations de copie par des appels à des services de bas niveau et archi-optimisés comme std::memcpy(), qui traitent l'objet à copier comme une simple séquence contiguë en mémoire de bytes.
Une classe C++ est « trivialement copiable » si :
Les règles pour être trivialement déplaçable sont analogues à celles pour être trivialement copiables. Dans la plupart des cas, pour une clase trivialement copiable, le mouvement et la copie sont une seule et même chose.
Un destructeur est trivial si :
Ici encore, la trivialité apparaît comme une caractéristique virale.
Une classe a une « disposition standard » si :
L'idée derrière le concept de disposition standard est de faciliter l'interopérabilité avec d'autres langages de programmation. La disposition exacte des attributs dans un type de disposition standard est documentée à même le standard (§9.2 aux dernières nouvelles).
Notez que l'héritage multiple est permis pour une classe de disposition standard dans la mesure où l'ensemble des règles qui précèdent sont respectées, ce qui reflète le fait que l'héritage de classes de disposition standard soit en quelque sorte équivalente à de la composition.
L'avènement de C++ 14 apporte aussi un raffinement au concept d'agrégat : un type qui initialiserait implicitement ses attributs d'instance est désormais un agrégat. Conséquemment, ce qui suit est considéré comme un agrégat depuis C++ 14 mais ne l'était pas avec C++ 11 :
#include <type_traits>
struct Point {
int x = 0, y = 0; // Point est un agrégat pour C++ 14, mais pas pour C++ 11
bool operator==(const Point &autre) const noexcept {
return x == autre.x && y == autre.y;
}
bool operator!=(const Point &autre) const noexcept {
return !(*this == autre);
}
};
int main() {
static_assert(std::is_pod_v<Point>);
static_assert(std::is_trivial_v<Point>);
static_assert(std::is_trivially_copyable_v<Point>);
static_assert(std::is_standard_layout_v<Point>);
}
Cependant, pour l'avoir testé sur plusieurs compilateurs, ce comportement n'est pas correctement supporté au moment d'écrire ces lignes : tous les compilateurs sur lesquels j'ai testé ce code indiquent que Point n'est ni un POD, ni trivial. Par contre, retirer l'initialisation implicite de x et de y lui confère sur tous les compilateurs les quatres caractéristiques testées statiquement ici.
Quelques liens supplémentaires pour enrichir le propos.