En C++, la gestion des ressources est un aspect fondamental de la programmation orientée objet. Les mécanismes de contrôle de copie sont essentiels pour définir comment les objets se comportent lorsqu'ils sont copiés, affectés ou déplacés. Ces mécanismes, notamment le constructeur de copie, l'opérateur d'affectation par copie, et plus récemment les références de valeur droite et le constructeur de déplacement introduits avec C++11, garantissent l'intégrité et l'efficacité des données.
Le Constructeur de Copie
Un constructeur de copie est un constructeur spécial qui est appelé lorsqu'un nouvel objet est créé à partir d'un objet existant du même type. Sa signature est caractérisée par un premier paramètre qui est une référence constante à une instance de la classe elle-même (par exemple, MaClasse(const MaClasse& autre)), et tous les paramètres additionnels doivent posséder des valeurs par défaut.
Le constructeur de copie synthétisé (par défaut) effectue une copie membre par membre. Cela fonctionne parfaitement pour les membres de type valeur (comme int, float) ou les objets qui gèrent correctement leur propre copie (comme std::string, std::vector). Cependant, pour les classes qui gèrent des ressources dynamiques (telles que des pointeurs vers de la mémoire allouée dynamiquement), un constructeur de copie par défaut peut entraîner des problèmes comme le "double free" ou des fuites de mémoire, car il copierait simplement l'adresse du pointeur au lieu de la ressource sous-jacente.
Voici les situations courantes où le constructeur de copie est invoqué :
- Lorsqu'un objet est passé par valeur à une fonction.
- Lorsqu'un objet est retourné par valeur depuis une fonction.
- Lors de l'initialisation d'un objet avec un autre objet de même type (par exemple,
MonObjet obj2 = obj1;ouMonObjet obj2(obj1);). - Lorsqu'un objet est inséré dans un conteneur qui le copie (par exemple,
std::vector::push_back).
Considérons une classe simple gérant un identifiant ISBN pour illustrer le concept :
#include <iostream>
#include <cstring> // Pour strlen et strcpy
#include <string> // Pour std::string
#include <vector> // Pour démonstration dans main
class Livre {
private:
char* identifiantISBN;
double prix;
public:
// Constructeur principal
Livre(const char* isbnVal, double prixVal = 0.0) : prix(prixVal) {
if (isbnVal) {
identifiantISBN = new char[std::strlen(isbnVal) + 1];
std::strcpy(identifiantISBN, isbnVal);
} else {
identifiantISBN = nullptr;
}
std::cout << "Constructeur principal appelé pour ISBN: " << (identifiantISBN ? identifiantISBN : "N/A") << std::endl;
}
// Constructeur de copie
Livre(const Livre& autre) : prix(autre.prix) {
if (autre.identifiantISBN) {
identifiantISBN = new char[std::strlen(autre.identifiantISBN) + 1];
std::strcpy(identifiantISBN, autre.identifiantISBN);
} else {
identifiantISBN = nullptr;
}
std::cout << "Constructeur de copie appelé pour ISBN: " << (identifiantISBN ? identifiantISBN : "N/A") << std::endl;
}
// Destructeur
~Livre() {
if (identifiantISBN) {
std::cout << "Destructeur appelé pour ISBN: " << identifiantISBN << std::endl;
delete[] identifiantISBN;
identifiantISBN = nullptr; // Pour éviter les pointeurs sauvages
} else {
std::cout << "Destructeur appelé pour ISBN: N/A (nullptr)" << std::endl;
}
}
// Méthode d'affichage
void afficherDetails() const {
std::cout << "ISBN: " << (identifiantISBN ? identifiantISBN : "N/A") << ", Prix: " << prix << std::endl;
}
// Accesseur pour l'ISBN (utile pour la démo)
const char* getISBN() const {
return identifiantISBN;
}
};
// Fonction prenant un objet par valeur, déclenchant le constructeur de copie
void traiterLivre(Livre livre) {
std::cout << "Dans traiterLivre: ";
livre.afficherDetails();
}
int main() {
std::cout << "--- Début main ---" << std::endl;
Livre monRoman("978-207036040", 25.50); // Appel du constructeur principal
std::cout << "Adresse de monRoman: " << &monRoman << std::endl;
std::cout << "\n--- Appel de traiterLivre (passage par valeur) ---" << std::endl;
traiterLivre(monRoman); // Déclenche le constructeur de copie pour le paramètre 'livre'
std::cout << "\n--- Initialisation par copie (Livre autreLivre = monRoman) ---" << std::endl;
Livre autreLivre = monRoman; // Déclenche le constructeur de copie
autreLivre.afficherDetails();
std::cout << "\n--- Fin main ---" << std::endl;
return 0;
}
Il est impératif que le premier paramètre d'un constructeur de copie soit une référence (et de préférence constante, const). Si ce n'était pas le cas, le passage de l'argument à ce constructeur nécessiterait lui-même une copie, ce qui déclencherait un appel récursif infini au constructeur de copie, entraînant une erreur de compilation ou un débordement de pile.
L'Opérateur d'Affectation par Copie
Contrairement au constructeur de copie qui initialise un nouvel objet, l'opérateur d'affectation par copie est invoqué lorsqu'un objet existant est affecté à un autre objet existant. Il est surchargé via la fonction membre operator=.
Lors de l'implémentation d'un opérateur d'affectation par copie pour une classe gérant des ressources, il est crucial de gérer correctement trois aspects :
- L'auto-affectation (
obj = obj;) : L'objet ne doit pas se détruire lui-même avant de tenter de copier ses propres données. - La libération des ressources existantes de l'objet de destination avant d'allouer de nouvelles ressources pour la copie.
- L'allocation de nouvelles ressources pour la copie.
La convention est de retourner une référence à l'objet courant (*this) pour permettre l'affectation en chaîne (obj1 = obj2 = obj3;).
#include <iostream>
#include <cstring> // Pour strlen et strcpy
class Article {
private:
char* reference;
int quantite;
public:
// Constructeur par défaut (nécessaire pour Article B; B = A;)
Article() : reference(nullptr), quantite(0) {
std::cout << "Constructeur par défaut appelé." << std::endl;
}
// Constructeur principal
Article(const char* refVal, int qty = 0) : quantite(qty) {
if (refVal) {
reference = new char[std::strlen(refVal) + 1];
std::strcpy(reference, refVal);
} else {
reference = nullptr;
}
std::cout << "Constructeur principal appelé pour référence: " << (reference ? reference : "N/A") << std::endl;
}
// Destructeur
~Article() {
if (reference) {
std::cout << "Destructeur appelé pour référence: " << reference << std::endl;
delete[] reference;
reference = nullptr;
} else {
std::cout << "Destructeur appelé pour référence: N/A (nullptr)" << std::endl;
}
}
// Constructeur de copie (pour la règle de trois/cinq)
Article(const Article& autre) : quantite(autre.quantite) {
if (autre.reference) {
reference = new char[std::strlen(autre.reference) + 1];
std::strcpy(reference, autre.reference);
} else {
reference = nullptr;
}
std::cout << "Constructeur de copie appelé pour référence: " << (reference ? reference : "N/A") << std::endl;
}
// Opérateur d'affectation par copie
Article& operator=(const Article& autre) {
std::cout << "Opérateur d'affectation par copie appelé." << std::endl;
// 1. Gérer l'auto-affectation
if (this == &autre) {
return *this;
}
// 2. Libérer les ressources existantes
if (reference) {
delete[] reference;
reference = nullptr;
}
// 3. Allouer et copier les nouvelles ressources
quantite = autre.quantite;
if (autre.reference) {
reference = new char[std::strlen(autre.reference) + 1];
std::strcpy(reference, autre.reference);
} else {
reference = nullptr;
}
return *this; // Retourner une référence à l'objet courant
}
void afficherInfo() const {
std::cout << "Réf: " << (reference ? reference : "N/A") << ", Qté: " << quantite << std::endl;
}
};
int main() {
std::cout << "--- Début démo affectation ---" << std::endl;
Article articleA("REF-001", 100); // Constructeur principal
articleA.afficherInfo();
Article articleB; // Constructeur par défaut
std::cout << "Avant affectation B = A:" << std::endl;
articleB.afficherInfo();
articleB = articleA; // Appel de l'opérateur d'affectation par copie
std::cout << "Après affectation B = A:" << std::endl;
articleB.afficherInfo();
std::cout << "\n--- Test d'auto-affectation ---" << std::endl;
articleA = articleA; // Auto-affectation
articleA.afficherInfo();
Article articleC("REF-003", 50); // Constructeur principal
articleC = articleA; // Affectation
articleC.afficherInfo();
std::cout << "--- Fin démo affectation ---" << std::endl;
return 0;
}
Contrôle Explicite : = default et = delete
Depuis C++11, il est possible de spécifier explicitement le comportement des fonctions membres spéciales (constructeurs, destructeur, opérateurs d'affectation) en utilisant = default et = delete.
-
= default: Force le compilateur à générer la version synthétisée (par défaut) de la fonction membre. Cela est utile si vous avez déjà défini d'autres constructeurs et que vous souhaitez toujours que le constructeur de copie ou par défaut soit généré.class Produit { public: std::string nom; double poids; Produit() = default; // Constructeur par défaut synthétisé Produit(const Produit&) = default; // Constructeur de copie synthétisé Produit& operator=(const Produit&) = default; // Opérateur d'affectation par copie synthétisé ~Produit() = default; // Destructeur synthétisé }; -
= delete: Empêche le compilateur de générer ou d'utiliser une fonction membre. C'est particulièrement utile pour les classes qui ne doivent pas être copiées (par exemple, des classes de mutex ou des objets qui encapsulent une ressource unique).class RessourceUnique { public: RessourceUnique() = default; // Interdire la copie RessourceUnique(const RessourceUnique&) = delete; RessourceUnique& operator=(const RessourceUnique&) = delete; // ... autres membres };Il n'est pas permis de supprimer un destructeur avec
= deletecar cela empêcherait la destruction de l'objet, même après qu'il ait été déplacé.
Références de Valeur Droite (Rvalue References) et Sémantique de Déplacement
Avant C++11, les opérations de copie pour les objets complexes gérant des ressources dynamiques pouvaient être coûteuses. Chaque copie entraînait une allocation de mémoire et une copie profonde des données, même lorsque l'objet source était une valeur temporaire qui allait être immédiatement détruite. La sémantique de déplacement, introduite avec les références de valeur droite, résout ce problème en permettant de "voler" les ressources d'un objet temporaire plutôt que de les copier.
Imaginez un déménagement : au lieu de racheter tous les meubles pour votre nouvelle maison (copie profonde), vous transportez simplement vos meubles existants de l'ancienne à la nouvelle adresse (déplacement). L'ancienne adresse est alors vide et peut être rasée sans problème.
Une référence de valeur droite (notée &&) est une référence qui ne peut être liée qu'à une expression qui est une valeur droite (rvalue), c'est-à-dire un objet temporaire ou qui n'a pas d'emplacement de stockage persistant. Les rvalues sont transitoires, elles n'existent généralement que le temps d'une expression. En revanche, les valeurs gauches (lvalues) ont une identité et une adresse mémoire persistantes.
Exemples de rvalues : littéraux (42, "hello"), résultats d'expressions arithmétiques (x + y), retours de fonctions par valeur.
int valeur = 10;
int&& refRvalue1 = 20; // Correct: 20 est une rvalue littérale
// int&& refRvalue2 = valeur; // Erreur: valeur est une lvalue
int&& refRvalue3 = valeur * 2; // Correct: valeur * 2 est une rvalue (résultat temporaire)
Pour lier une référence de valeur droite à une valeur gauche nommée (qui est une lvalue), il faut explicitement la convertir en rvalue à l'aide de std::move. L'utilisation de std::move indique que le programme n'a plus l'intention d'utiliser l'objet source, signalant qu'il est sûr d'en "voler" les ressources.
int x = 50;
int&& refRvalue4 = std::move(x); // Correct: 'x' est traité comme une rvalue
// À partir de ce point, 'x' ne doit plus être utilisé de manière valide,
// sauf pour sa destruction ou une nouvelle affectation.
L'adresse mémoire d'un objet ne change pas lorsqu'une référence de valeur droite est utilisée, elle crée simplement un alias pour un objet exsitant, temporaire ou non.
#include <iostream>
#include <vector>
#include <string>
class TempClass {
public:
TempClass() { std::cout << "TempClass CTOR" << std::endl; }
~TempClass() { std::cout << "TempClass DTOR" << std::endl; }
};
TempClass createTemp() {
return TempClass();
}
int main() {
std::cout << "--- Démo Références de Valeur Droite ---" << std::endl;
int nombre = 42;
std::cout << "Adresse de nombre: " << &nombre << std::endl;
int&& rrefNombre = std::move(nombre); // 'nombre' est maintenant "déplacé"
std::cout << "Adresse de rrefNombre: " << &rrefNombre << std::endl;
std::cout << "Valeur via rrefNombre: " << rrefNombre << std::endl;
// std::cout << "Valeur via nombre: " << nombre << std::endl; // DANGEREUX! 'nombre' est dans un état valide mais indéterminé
std::cout << "\nAppel de createTemp():" << std::endl;
TempClass&& tempObj = createTemp(); // Lie à l'objet temporaire retourné par la fonction
// L'objet temporaire est lié à tempObj et sa durée de vie est étendue.
std::cout << "Fin démo Références de Valeur Droite." << std::endl;
return 0;
}
Le Constructeur de Déplacement
Le constructeur de déplacement est une fonction membre spéciale qui prend une référence de valeur droite à un objet du même type. Son objectif est de transférer efficacement la propriété des ressources d'un objet source temporaire ou "déplacé" vers un nouvel objet, sans effectuer de copie coûteuse. Après le déplacement, l'objet source est laissé dans un état valide mais non spécifié, et ses ressources sont généralement nullifiées pour éviter leur double libération lors de sa destruction.
Le mot-clé noexcept est souvent ajouté au constructeur de déplacement pour indiquer qu'il ne lèvera pas d'exceptions. Cela permet aux conteneurs de la STL d'effectuer des optimisations (par exemple, pour std::vector, redimensionner sans craindre d'exceptions en cas de déplacement).
#include <iostream>
#include <cstring> // Pour strlen et strcpy
#include <utility> // Pour std::move
#include <vector> // Pour démonstration dans main
class Document {
private:
char* contenu; // Représente une ressource potentiellement volumineuse
int taille;
public:
// Constructeur par défaut
Document() : contenu(nullptr), taille(0) {
std::cout << "Document CTOR par défaut." << std::endl;
}
// Constructeur principal
Document(const char* texte, int sz) : taille(sz) {
if (texte) {
contenu = new char[std::strlen(texte) + 1];
std::strcpy(contenu, texte);
} else {
contenu = nullptr;
}
std::cout << "Document CTOR principal pour: '" << (contenu ? contenu : "N/A") << "'" << std::endl;
}
// Destructeur
~Document() {
if (contenu) {
std::cout << "Document DTOR pour: '" << contenu << "'" << std::endl;
delete[] contenu;
contenu = nullptr;
} else {
std::cout << "Document DTOR pour nullptr (déplacé ou par défaut)." << std::endl;
}
}
// Constructeur de copie
Document(const Document& autre) : taille(autre.taille) {
if (autre.contenu) {
contenu = new char[std::strlen(autre.contenu) + 1];
std::strcpy(contenu, autre.contenu);
} else {
contenu = nullptr;
}
std::cout << "Document CTOR de copie pour: '" << (contenu ? contenu : "N/A") << "'" << std::endl;
}
// Opérateur d'affectation par copie
Document& operator=(const Document& autre) {
std::cout << "Document Opérateur d'affectation par copie." << std::endl;
if (this == &autre) {
return *this;
}
if (contenu) {
delete[] contenu;
}
taille = autre.taille;
if (autre.contenu) {
contenu = new char[std::strlen(autre.contenu) + 1];
std::strcpy(contenu, autre.contenu);
} else {
contenu = nullptr;
}
return *this;
}
// Constructeur de déplacement
// 'noexcept' est crucial pour les conteneurs STL
Document(Document&& autre) noexcept
: contenu(autre.contenu), taille(autre.taille) { // Vole les ressources
autre.contenu = nullptr; // Met l'objet source dans un état valide mais vide
autre.taille = 0;
std::cout << "Document CTOR de déplacement pour: '" << (contenu ? contenu : "N/A") << "'" << std::endl;
}
// Opérateur d'affectation de déplacement (Rule of Five)
Document& operator=(Document&& autre) noexcept {
std::cout << "Document Opérateur d'affectation de déplacement." << std::endl;
if (this == &autre) {
return *this;
}
// Libérer les ressources existantes de l'objet cible
if (contenu) {
delete[] contenu;
}
// Vole les ressources de l'objet source
contenu = autre.contenu;
taille = autre.taille;
// Met l'objet source dans un état valide et vide
autre.contenu = nullptr;
autre.taille = 0;
return *this;
}
void afficher() const {
std::cout << "Contenu: '" << (contenu ? contenu : "VIDE") << "', Taille: " << taille << std::endl;
}
};
int main() {
std::cout << "--- Démo Constructeur de Déplacement ---" << std::endl;
Document docOriginal("Ceci est un texte long pour un document.", 50); // CTOR principal
docOriginal.afficher();
std::cout << "\n--- Déplacement de docOriginal vers docNouveau ---" << std::endl;
Document docNouveau = std::move(docOriginal); // Appel du constructeur de déplacement
docNouveau.afficher();
docOriginal.afficher(); // docOriginal est maintenant vide
std::cout << "\n--- Création d'un document temporaire pour affectation de déplacement ---" << std::endl;
Document docAffecte; // CTOR par défaut
Document docTmp("Autre texte", 20); // CTOR principal
docAffecte = std::move(docTmp); // Appel de l'opérateur d'affectation de déplacement
docAffecte.afficher();
docTmp.afficher(); // docTmp est maintenant vide
std::cout << "\n--- Utilisation avec std::vector::push_back ---" << std::endl;
std::vector<document> collectionDocs;
collectionDocs.reserve(2); // Pour éviter les réallocations qui appelleraient des CTOR de copie/déplacement
std::cout << "Push_back d'un document temporaire (move implicit):" << std::endl;
collectionDocs.push_back(Document("Doc temporaire 1", 10)); // Move CTOR appelé implicitement
collectionDocs.back().afficher();
std::cout << "Push_back d'un lvalue 'docOriginal' déplacé:" << std::endl;
collectionDocs.push_back(std::move(docNouveau)); // Move CTOR appelé
collectionDocs.back().afficher();
docNouveau.afficher(); // docNouveau est maintenant vide
std::cout << "\n--- Fin Démo Constructeur de Déplacement ---" << std::endl;
return 0;
}
</document>