1. Ressources dynamiques et durée de vie
Une classe C++ peut posséder une ressource : mémoire dynamique, buffer, handle Windows, socket, descripteur POSIX, etc. Le point essentiel est de savoir qui possède cette ressource et qui doit la libérer.
Code: Select all
class Buffer
{
private:
int* data;
size_t size;
public:
Buffer(size_t s) : size(s)
{
data = new int[size];
}
~Buffer()
{
delete[] data;
}
};
2. Copie superficielle et copie profonde
Une copie automatique membre par membre d'un objet contenant un pointeur peut copier seulement l'adresse.
Code: Select all
Buffer a(100);
Buffer b = a;
Une copie profonde crée au contraire une nouvelle ressource et copie son contenu.
3. Constructeur de copie
Le constructeur de copie crée un nouvel objet à partir d'un autre objet existant.
Code: Select all
ClassName(const ClassName& other);
Code: Select all
Buffer::Buffer(const Buffer& other) : size(other.size)
{
data = new int[size];
for (size_t i = 0; i < size; ++i)
data[i] = other.data[i];
}
4. Opérateur d'affectation par copie
Il faut distinguer construction et affectation.
Code: Select all
Buffer b = a; // construction par copie
Buffer c(100);
c = a; // affectation par copie
Code: Select all
Buffer& operator=(const Buffer& other);
Code: Select all
a = a;
Copier une grosse ressource peut coûter cher. Lorsqu'un objet source est temporaire ou que son ancien contenu n'est plus nécessaire, C++ permet de transférer sa ressource au lieu de la recopier.
Avant :
Code: Select all
A ----> grosse allocation
Code: Select all
A ----> vide
B ----> grosse allocation
6. Lvalues, rvalues et références rvalue
Une variable nommée telle que :
Code: Select all
Buffer buffer(100);
Code: Select all
Buffer(100)
Les références rvalue utilisent && :
Code: Select all
Buffer&&
7. Constructeur de déplacement
Sa forme classique est :
Code: Select all
Buffer(Buffer&& other) noexcept;
Code: Select all
Buffer::Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size)
{
other.data = nullptr;
other.size = 0;
}
8. Affectation par déplacement
L'affectation par déplacement transfère une ressource vers un objet qui existe déjà.
Code: Select all
Buffer& Buffer::operator=(Buffer&& other) noexcept
{
if (this != &other)
{
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
std::move ne déplace pas lui-même la mémoire. Il permet de traiter une expression comme une rvalue afin qu'une opération de déplacement puisse être sélectionnée.
Code: Select all
Buffer a(1000);
Buffer b(std::move(a));
10. Passage optimal des paramètres
Passage par valeur :
Code: Select all
void process(Buffer buffer);
Passage par référence :
Code: Select all
void process(Buffer& buffer);
Passage par référence constante :
Code: Select all
void process(const Buffer& buffer);
Référence rvalue :
Code: Select all
void process(Buffer&& buffer);
11. Rule of Five
Une classe qui possède directement une ressource peut devoir contrôler cinq fonctions spéciales :
- le destructeur ;
- le constructeur de copie ;
- l'opérateur d'affectation par copie ;
- le constructeur de déplacement ;
- l'opérateur d'affectation par déplacement.
Code: Select all
~T();
T(const T&);
T& operator=(const T&);
T(T&&);
T& operator=(T&&);
12. Rule of Zero
Si les membres d'une classe gèrent déjà correctement leurs ressources, il vaut souvent mieux laisser le compilateur générer les opérations spéciales.
Par exemple :
Code: Select all
class Buffer
{
private:
std::vector<int> data;
};
Pour du bas niveau, la règle importante reste : le propriétaire d'une ressource doit avoir une politique de durée de vie claire.
13. Fonctions membres static
Une fonction membre normale agit sur une instance et dispose implicitement de this. Une fonction static appartient à la classe et ne possède pas de pointeur this.
Code: Select all
class Counter
{
public:
static int getGlobalCount();
};
Code: Select all
Counter::getGlobalCount();
14. Fonctions membres const
Le const placé après une fonction membre signifie que la fonction promet de ne pas modifier normalement l'état de l'objet.
Code: Select all
size_t getSize() const;
Code: Select all
const Buffer buffer(100);
buffer.getSize();
15. Surcharge basée sur const
Une classe peut proposer une version modifiable et une version const d'une fonction.
Code: Select all
T& get();
const T& get() const;
16. mutable
Un membre mutable peut être modifié depuis une fonction const.
Code: Select all
class Data
{
private:
mutable int accessCount = 0;
public:
int getValue() const
{
++accessCount;
return 42;
}
};
17. Surcharge des fonctions membres
Plusieurs fonctions peuvent avoir le même nom si leurs paramètres permettent au compilateur de les distinguer.
Code: Select all
void setValue(int value);
void setValue(double value);
18. Fonctions membres qualifiées par référence
Une fonction membre peut être réservée aux lvalues ou aux rvalues.
Code: Select all
void function() &;
void function() &&;
Code: Select all
Object obj;
obj.function(); // version &
Object().function(); // version &&
19. Fonctions inline
Une fonction définie directement dans une classe est implicitement inline.
Code: Select all
class Counter
{
public:
int get() const
{
return value;
}
private:
int value;
};
20. constexpr
constexpr indique qu'une valeur ou une fonction peut participer à une évaluation à la compilation lorsque les conditions le permettent.
Code: Select all
constexpr int square(int x)
{
return x * x;
}
constexpr int result = square(5);
Code: Select all
int x;
std::cin >> x;
int result = square(x);
21. consteval
consteval est plus strict que constexpr. Une fonction immédiate consteval doit être évaluée à la compilation lorsqu'elle est appelée.
Code: Select all
consteval int square(int x)
{
return x * x;
}
Code: Select all
constexpr -> peut être évalué à la compilation
consteval -> doit être évalué à la compilation
Un type peut être conçu pour créer des objets utilisables dans des expressions constantes.
Code: Select all
class Point
{
public:
constexpr Point(int x, int y) : x(x), y(y) {}
constexpr int getX() const
{
return x;
}
private:
int x;
int y;
};
constexpr Point p(10, 20);
Une donnée static appartient à la classe plutôt qu'à chaque objet.
Code: Select all
class Process
{
private:
static int count;
};
24. Variables inline static
Le C++ moderne permet d'initialiser certaines données statiques directement dans la classe.
Code: Select all
class Counter
{
public:
inline static int count = 0;
};
25. Membres static constexpr
Une constante liée logiquement à une classe peut être déclarée ainsi :
Code: Select all
class Buffer
{
public:
static constexpr size_t MaxSize = 4096;
};
Code: Select all
Buffer::MaxSize
26. Membres références
Une classe peut stocker une référence vers un autre objet.
Code: Select all
class Manager
{
private:
Application& app;
public:
Manager(Application& a) : app(a) {}
};
La classe ne possède pas nécessairement l'objet référencé. Il faut donc garantir que l'objet référencé reste vivant suffisamment longtemps. Sinon, la classe contiendra une référence pendante.
27. Classes imbriquées
Une classe peut être déclarée à l'intérieur d'une autre.
Code: Select all
class Process
{
public:
class Thread
{
public:
int id;
};
};
Code: Select all
Process::Thread
28. Énumérations dans les classes
Une énumération peut être placée dans une classe.
Code: Select all
class Device
{
public:
enum class State
{
Stopped,
Running,
Error
};
};
Code: Select all
Device::State::Running
29. Surcharge des opérateurs
C++ permet de donner un comportement aux opérateurs pour des types utilisateur.
Une classe numérique peut par exemple permettre :
Code: Select all
a + b
Code: Select all
a.add(b)
30. Opérateur membre ou fonction non membre
Un opérateur peut être une fonction membre :
Code: Select all
Value Value::operator+(const Value& rhs) const;
Code: Select all
Value operator+(const Value& lhs, const Value& rhs);
31. Opérateurs arithmétiques
On peut surcharger +, -, * et /.
Code: Select all
class Number
{
public:
Number operator+(const Number& other) const
{
return Number(value + other.value);
}
private:
int value;
};
32. Opérateurs arithmétiques composés
Les opérateurs +=, -=, *= et /= peuvent également être surchargés.
Code: Select all
Number& operator+=(const Number& other)
{
value += other.value;
return *this;
}
Code: Select all
Number operator+(Number lhs, const Number& rhs)
{
lhs += rhs;
return lhs;
}
Les classes peuvent supporter :
Code: Select all
==
!=
<
<=
>
>=
Code: Select all
bool operator==(const Value& other) const
{
return value == other.value;
}
34. Comparaisons depuis C++20 et operator<=>
C++20 introduit l'opérateur <=>, surnommé spaceship operator.
Dans les cas simples :
Code: Select all
auto operator<=>(const MyClass&) const = default;
Cela réduit fortement le code répétitif.
35. = delete
Une opération peut être explicitement interdite.
Code: Select all
class Resource
{
public:
Resource(const Resource&) = delete;
Resource& operator=(const Resource&) = delete;
};
C'est très utile pour une ressource dont la duplication n'a pas de sens. On peut par exemple concevoir un objet déplaçable mais non copiable.
36. = default
On peut demander explicitement au compilateur de générer son comportement standard.
Code: Select all
MyClass() = default;
Code: Select all
bool operator==(const MyClass&) const = default;
37. Interfaces stables
Une classe publique devrait exposer ce dont ses utilisateurs ont besoin sans révéler inutilement son fonctionnement interne.
Conceptuellement :
Code: Select all
utilisateur
|
v
interface publique
|
v
implémentation privée
38. PImpl : Pointer to Implementation
Une technique classique consiste à déplacer les détails internes dans une classe d'implémentation séparée.
Conceptuellement :
Code: Select all
class PublicClass
{
private:
Impl* impl;
};
Code: Select all
PublicClass
|
+----> Impl
|
+---- données privées
+---- dépendances
+---- logique interne
39. Importance pour le développement système
Pour du C++ système, les notions les plus importantes de ce chapitre sont :
- durée de vie des objets ;
- possession des ressources ;
- copie profonde et copie superficielle ;
- constructeur et affectation par copie ;
- constructeur et affectation par déplacement ;
- std::move ;
- Rule of Five et Rule of Zero ;
- const ;
- références et durée de vie.
Code: Select all
double free
use-after-free
dangling pointer
dangling reference
resource leak
copie involontaire
destruction prématurée
Les ref-qualified member functions, consteval, classes imbriquées, PImpl et les surcharges avancées d'opérateurs sont utiles à connaître mais moins prioritaires pour commencer du Win32/POSIX/kernel.
40. Raisonner en propriétaire de ressource
Pour une ressource système, la question fondamentale est :
Code: Select all
Qui possède cette ressource ?
Code: Select all
A ----+
+----> RESOURCE
B ----+
Trois stratégies sont possibles selon le type.
Copie réelle :
Code: Select all
A ----> RESOURCE A
B ----> RESOURCE B
Code: Select all
A ----> RESOURCE
copie de A -> erreur de compilation
Code: Select all
avant :
A ----> RESOURCE
after move :
A ----> vide
B ----> RESOURCE
41. Résumé
Une classe C++ peut représenter directement la durée de vie d'une ressource.
Code: Select all
constructeur
|
v
acquisition
|
v
utilisation
|
v
destructeur
|
v
libération
Lors d'un déplacement, la propriété est transférée sans recopier inutilement la ressource.
Les cinq fonctions spéciales essentielles sont :
Code: Select all
~T()
T(const T&)
T& operator=(const T&)
T(T&&)
T& operator=(T&&)
À retenir également :
- const permet de garantir qu'une fonction membre ne modifie pas normalement l'objet ;
- static associe une donnée ou une fonction à la classe plutôt qu'à une instance ;
- constexpr permet l'évaluation à la compilation lorsque c'est possible ;
- consteval impose l'évaluation à la compilation ;
- = delete interdit explicitement une opération ;
- = default demande le comportement généré par le compilateur ;
- la surcharge d'opérateurs permet une syntaxe naturelle lorsqu'elle respecte la signification de l'opérateur ;
- PImpl permet de cacher fortement l'implémentation derrière une interface publique.
