Les catégories de classes

Il existe beaucoup de différences entre les structures en C et les structures ou classes en C++. La seule différence entre les mot-clés struct et class en C++ est la visibilité de leur contenu par défaut : public pour struct, et private pour class.

Pour qu'une structure en C++ ait les mêmes propriétés qu'une structure en C, il faut qu'elle soit trivial et standard layout : on parle alors de Plain Old Data (POD).

Les méthodes non virtuelles, et les variables ou fonctions statiques d'une classe n'affecte pas sa catégorie puisque cela ne change pas l'objet en lui-même.

Trivial

En C, les structures existent dès leur allocation et jusqu'à leur déallocation, alors qu'en C++ elles existent après leur constructeur, et jusqu'à leur destructeur. De plus, seul l'assignement par copie est généré par le compilateur pour les structures en C (afin de pouvoir écrire struct_t s1 = s2; ). En C++, le compilateur génère jusqu'à six fonctions :

  • Le constructeur par défaut : T::T()

  • Le constructeur par copie : T::T(T const& lhs)

  • Le constructeur par move : T::T(T&& lhs)

  • L'assignement par copie : T& T::operator=(T const& lhs)

  • L'assignement par move : T& T::operator=(T&& lhs)

  • Le destructeur : T::~T()

circle-info

On peut explicitement supprimer ou générer les fonctions du compilateur :

Lorsqu'elles sont définies par le compilateur, ces fonctions peuvent être trivial : dans ce cas, le constructeur par défaut et le destructeur ne font rien, et les opérations par copie ou move ne font que copier le contenu de l'objet de manière semblable à memcpy. Pour cela, il faut que la classe ne contienne pas de méthodes virtuelles, et ne possède ou n'hérite que de classes elles-même trivial.

Lorsque le concept TriviallyCopyablearrow-up-right est respecté, les objets peuvent être copiés avec memcpy et peuvent être sérialisés de manière semblable (comme avec ifstream.read() ou ofstream.write()). Lorsqu'en plus le constructeur par défaut est trivial, la classe est trivial.

Standard layout

Les classes en C++ ne sont pas forcément représentées de la même manière en mémoire que les classes en C. Afin d'avoir une classe standard layout, il faut qu'elle n'utilise pas d'héritage (ou alors de façon très restreinte), qu'elle ne soit composée que de membres eux-mêmes standard layout, qu'elle n'ait pas de fonctions virtuelles et que tous ses membres aient la même visibilité (tous public, private ou protected).

Ces classes peuvent être pratiques pour interagir avec un programme en C, et la macro offsetof(Type, m) permet d'obtenir l'offset entre l'adresse d'un objet Type et l'adresse de sa variable m.

La représentation en mémoire des classes peut être modifiée pour réduire leur taille en mémoire ou accélérer l'accès à leurs données, notamment en changeant l'ordre de déclaration des membresarrow-up-right ou en utilisant des bitfieldsarrow-up-right pour représenter plusieurs données sur un nombre précis de bits.

circle-info

sizeof(T) doit être un multiple de alignof(T), notamment pour stocker des objets de même type dans un tableau, mais cela peut alors augmenter la taille des objets.

circle-check

Les aggrégats et brace constructors

Si une classe ne possède que des membres publics et aucun constructeur, le compilateur génère un constructeur spécial pour cette classe, qui est alors un aggregate. Ce constructeur permet d'initialiser les membres de la classe dans l'ordre où ils sont déclarés. Cette catégorie est beaucoup plus large que les deux précédentes, parce que les membres des aggrégats n'ont pas à être eux-mêmes des aggrégats :

circle-check

Ce constructeur ne peut être appelé avec des parenthèses, mais doit utiliser des accolades : il s'agit du 'brace constructor'. Utiliser cette notation peut remplacer n'importe quel constructeur avec les parenthèses, avec quelques particularités :

  • Comme vu au-dessus, il peut également correspondre au constructeur d'aggégats

  • Il interdit les narrowing conversion de ses paramètres

  • Si il prend au moins un paramètre, il sélectionne en priorité les constructeurs prenant un std::initializer_list<T>

  • Il permet de ne pas mentionner le type de l'objet construit selon le contexte

La librairie standard contient des traits permettant de vérifier les catégories auxquelles appartient une classe donnée à la compilation : std::is_trivial, std::is_trivially_default_constructible (existe pour chacune des fonctions potentiellement trivial), std::is_standard_layout, std::is_aggregate, ou encore std::is_pod (déprécié, ne fait que combiner std::is_trivial et std::is_standard_layout).

Pour aller plus loin

Il existe également des traits pour voir si une classe possède des méthodes virtuelles (std::is_polymorphic), et si cette classe peut être instanciée (c'est-à-dire qu'aucune méthode virtuelle n'est pure, avec std::is_abstract).

Les représentations des objets en mémoire peuvent être manipulées pour permettre du type punningarrow-up-right (comme avec les union ou reinterpret_cast). Attention cependant, de nombreuses manipulations ne sont pas garanties par le standard et on se restreint généralement aux types TriviallyCopyable.

Un article de Stack Overflowarrow-up-right détaille plus précisément les catégories standard layout, trivial et aggregate.

Challenge

(**) Créer une classe tagged_ptr<T> qui permet de stocker et de manipuler un compteur à l'intérieur d'un pointeur de type T*. Exemple :

Bonus : rendre la classe covariante.

Last updated