Les catégories de classes
Last updated
Last updated
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.
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()
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 TriviallyCopyable 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.
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 membres ou en utilisant des bitfields pour représenter plusieurs données sur un nombre précis de bits.
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.
Ranger les membres d'une classe par contraintes d'alignement croissantes ou décroissantes assure la plus petite taille en mémoire possible pour le type donné.
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 :
Depuis C++17, les aggrégats peuvent avoir de l'héritage non virtuel public.
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
).
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 punning (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 Overflow détaille plus précisément les catégories standard layout, trivial et aggregate.
(**) 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.