# 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()`   &#x20;
* Le constructeur par copie : `T::T(T const& lhs)`   &#x20;
* Le constructeur par move : `T::T(T&& lhs)`   &#x20;
* L'assignement par copie : `T& T::operator=(T const& lhs)`   &#x20;
* L'assignement par move : `T& T::operator=(T&& lhs)`   &#x20;
* Le destructeur : `T::~T()`   &#x20;

![](https://1791579444-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LBO6MEV9vNd-HDuzO5k%2F-LC9eHAREPRCzTq_I2Gz%2F-LC9eMZPkzrAdllxkvIo%2Fm%C3%A9thodes%20g%C3%A9n%C3%A9r%C3%A9es%20pour%20les%20classes.jpg?alt=media\&token=1ab373a4-2b56-4d31-87c1-8540f6d692d1)

{% hint style="info" %}
On peut explicitement supprimer ou générer les fonctions du compilateur :

```cpp
struct Copyable {
    Copyable(Copyable&&) = delete;
    Copyable(Copyable const&) = default;
};
```

{% endhint %}

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*.&#x20;

Lorsque le concept [TriviallyCopyable](https://en.cppreference.com/w/cpp/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*.

```cpp
struct Trivial {
    int values[3];
private:
    char const* name;
};

struct NonTrivial {
    char data[16];
    NonTrivial() {}
};
```

### 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`.

```cpp
struct StandardLayout {
    int i;
    float f;
    StandardLayout(int value) : i(value + 1), f(value - 1.f) {}
};

struct NonStandardLayout {
    int i;
private:
    float f;
};
```

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](http://www.catb.org/esr/structure-packing/) ou [en utilisant des bitfields](https://en.cppreference.com/w/cpp/language/bit_field) pour représenter plusieurs données sur un nombre précis de bits.

```cpp
struct LargeStruct { // sizeof(LargeStruct) = 24
    char c1;
    long long l;
    char c2;
};

struct SmallStruct { // sizeof(SmallStruct) = 16
    int val;
    char c1;
    char c2;
    // 3 valeurs sur 16 bits
    unsigned short b1: 1;
    unsigned short b2: 6;
    unsigned short b3: 9;
    long long l;
};
```

{% hint style="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.
{% endhint %}

{% hint style="success" %}
&#x20;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é.
{% endhint %}

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

```cpp
struct Aggregate {
    int id;
    std::string message;
    int id2;
};
// id2 n'est pas spécifié et est donc initialisé par défaut (égal à zéro).
Aggregate agg{ 42, "Hello" };
```

{% hint style="success" %}
Depuis C++17, les aggrégats peuvent avoir de l'héritage non virtuel public.
{% endhint %}

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

```cpp
struct MyClass {
    MyClass(std::initializer_list<int> args);
    MyClass() = default;
    explicit MyClass(std::string const& name, float f);
    MyClass(int id);
}

MyClass make_object() {
    // Constructeur par défaut ; pas besoin de mentionner le type.
    return {};
    // Constructeur prenant une std::initializer_list<int>.
    return { 1 };
    // Constructeur prenant un int.
    return MyClass(1);
    // On doit spécifier le type pour les constructeurs explicites.
    return MyClass{ "hello", 3.14f };
}

void use_object(MyClass&& obj);

int main() {
    // Pas besoin de mentionner le type.
    use_object({ 1u, 1ll });
    // Narrowing conversion de float en int non autorisée.
    // use_object({ 1, 1.f });
}
```

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 punning](https://en.wikipedia.org/wiki/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](https://stackoverflow.com/a/4178176/8548903) 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 :

```cpp
// Change l'objet pointé par ptr et incrémente son compteur.
// L'ancienne valeur du compteur est renvoyée.
int actualize(tagged_ptr<T>& ptr, T const& value) {
    static_assert(sizeof(T*) == sizeof(ptr));
    int counter = ptr.get_counter();
    *ptr = value;
    ptr.increment_counter();
    return counter;
}
```

Bonus : rendre la classe covariante.
