À propos des agrégats et des POD (Plain Old Datatypes)

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

Être considéré comme un agrégat

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
class A0 {
public:
  char s[10];   // Ok, public
  A0& operator=(const A0 &autre) {/* */} // Ok, permis
private:
  void f() { } // Ok, méthode non-virtuelle
};
struct A1 { // Ok
private:
   static const int N = 3; // Ok, static
};
class A2 {
public:
   int n; // Ok
};
struct NA0 {
  virtual void f() {} // Non, méthode virtuelle
  virtual ~NA0() = default; // Non, destructeur virtuel
};

class NA1 {
  int n; // Non, privé
};

class NA2 {
public:
  NA2(int) {} // Non, défini par l'usager
};

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

Utilité des agrégats

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.

L'aggregate initialization

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.

La value-initialization

Un objet est value-initialized s'il est initialisé au zéro de son type. Dans le cas de ptsInc[4] à droite, les attributs x et y vaudront donc zéro, et il en sera de même pour tous les éléments de ptsZ.

struct Point {
   int x, y;
};
int main() {
   Point p = { 0, 0 }; // légal même avec C++ 03
   Point pts[] = {
      { 1,0 }, { 0, 1 }, { -1, 0 }, { 0, -1 }
   }; // idem
   Point ptsInc[5] = {
      { 1,0 }, { 0, 1 }, { -1, 0 }, { 0, -1 }
   };
   Point ptsZ[5] = {};
   Point pt = { 1 }; // pt.y == 0
}

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.

union opts {
   char c;
   int n;
};
int main() {
   opts o = {}; // value-initialization de o.c
}

Être considéré comme un POD (Plain Old Datatype)

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) :

Agrégats depuis C++ 11

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.

POD depuis C++ 11

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.

Être triviale

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.

Opérations triviales

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.

Être trivialement copiable

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 :

Être trivialement déplaçable

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.

Être trivialement destructible

Un destructeur est trivial si :

Ici encore, la trivialité apparaît comme une caractéristique virale.

Avoir une disposition standard (Standard Layout)

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.

Agrégats depuis C++ 14

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.

Lectures complémentaires

Quelques liens supplémentaires pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !