Namespaces
Variants

The rule of three/five/zero

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

Table des matières

Règle de trois

Si une classe nécessite un destructeur défini par l'utilisateur, un constructeur de copie défini par l'utilisateur, ou un opérateur d'affectation de copie défini par l'utilisateur, elle nécessite presque certainement les trois.

Parce que C++ copie et copie-assigne les objets de types définis par l'utilisateur dans diverses situations (passage/retour par valeur, manipulation d'un conteneur, etc.), ces fonctions membres spéciales seront appelées, si elles sont accessibles, et si elles ne sont pas définies par l'utilisateur, elles sont implicitement définies par le compilateur.

Les fonctions membres spéciales définies implicitement ne doivent pas être utilisées si la classe gère une ressource dont le gestionnaire est un objet de type non-classe (pointeur brut, descripteur de fichier POSIX, etc), dont le destructeur ne fait rien et le constructeur de copie/opérateur d'affectation effectue une "copie superficielle" (copie la valeur du gestionnaire, sans dupliquer la ressource sous-jacente).

#include <cstddef>
#include <cstring>
#include <iostream>
#include <utility>
class rule_of_three
{
    char* cstring; // pointeur brut utilisé comme handle vers un
                   // bloc de mémoire alloué dynamiquement
public:
    explicit rule_of_three(const char* s = "") : cstring(nullptr)
    {   
        if (s)
        {   
            cstring = new char[std::strlen(s) + 1]; // alloue
            std::strcpy(cstring, s); // peuple
        }
    }
    ~rule_of_three() // I. destructeur
    {
        delete[] cstring; // désalloue
    }
    rule_of_three(const rule_of_three& other) // II. constructeur de copie
        : rule_of_three(other.cstring) {}
    rule_of_three& operator=(const rule_of_three& other) // III. assignation de copie
    {
        // implémenté via copy-and-swap par concision
        // notez que cela empêche la réutilisation potentielle du stockage
        rule_of_three temp(other);
        std::swap(cstring, temp.cstring);
        return *this;
    }
    const char* c_str() const // accesseur
    {
        return cstring;
    }
};
int main()
{
    rule_of_three o1{"abc"};
    std::cout << o1.c_str() << ' ';
    auto o2{o1}; // II. utilise le constructeur de copie
    std::cout << o2.c_str() << ' ';
    rule_of_three o3("def");
    std::cout << o3.c_str() << ' ';
    o3 = o2; // III. utilise l'assignation de copie
    std::cout << o3.c_str() << '\n';
}   // I. tous les destructeurs sont appelés ici

Sortie :

abc abc def abc

Les classes qui gèrent des ressources non copiables via des handles copiables peuvent devoir déclarer l'assignation de copie et le constructeur de copie private et ne pas fournir leurs définitions (jusqu'à C++11) définir l'assignation de copie et le constructeur de copie comme = delete (depuis C++11) . Ceci est une autre application de la règle de trois : supprimer l'un et laisser l'autre être implicitement défini est généralement incorrect.

Règle de cinq

Parce que la présence d'un destructeur, d'un constructeur de copie ou d'un opérateur d'affectation par copie défini par l'utilisateur (incluant = default ou = delete déclarés) empêche la définition implicite du move constructor et du move assignment operator , toute classe pour laquelle la sémantique de déplacement est souhaitable doit déclarer les cinq fonctions membres spéciales :

class rule_of_five
{
    char* cstring; // pointeur brut utilisé comme gestionnaire d'un
                   // bloc de mémoire alloué dynamiquement
public:
    explicit rule_of_five(const char* s = "") : cstring(nullptr)
    { 
        if (s)
        {
            cstring = new char[std::strlen(s) + 1]; // allouer
            std::strcpy(cstring, s); // peupler
        } 
    {
    ~rule_of_five()
    {
        delete[] cstring; // désallouer
    {
    rule_of_five(const rule_of_five& other) // constructeur de copie
        : rule_of_five(other.cstring) {}
    rule_of_five(rule_of_five&& other) noexcept // constructeur de déplacement
        : cstring(std::exchange(other.cstring, nullptr)) {}
    rule_of_five& operator=(const rule_of_five& other) // affectation par copie
    {
        // implémenté comme affectation par déplacement depuis une copie temporaire par concision
        // notez que cela empêche la réutilisation potentielle du stockage
        return *this = rule_of_five(other);
    {
    rule_of_five& operator=(rule_of_five&& other) noexcept // affectation par déplacement
    {
        std::swap(cstring, other.cstring);
        return *this;
    {
// alternativement, remplacer les deux opérateurs d'affectation par une implémentation
// copy-and-swap, qui échoue également à réutiliser le stockage dans l'affectation par copie.
//  rule_of_five& operator=(rule_of_five other) noexcept
//  {
//      std::swap(cstring, other.cstring);
//      return *this;
//  }
{;

Contrairement à la Règle des Trois, l'absence de constructeur de déplacement et d'opérateur d'affectation par déplacement n'est généralement pas une erreur, mais une opportunité d'optimisation manquée.

Règle de zéro

Les classes qui possèdent des destructeurs personnalisés, des constructeurs de copie/déplacement ou des opérateurs d'affectation par copie/déplacement doivent traiter exclusivement de la propriété (ce qui découle du principe de responsabilité unique ). Les autres classes ne devraient pas avoir de destructeurs personnalisés, de constructeurs de copie/déplacement ou d'opérateurs d'affectation par copie/déplacement [1] .

Cette règle apparaît également dans les C++ Core Guidelines sous le nom de C.20: Si vous pouvez éviter de définir les opérations par défaut, faites-le .

class rule_of_zero
{
    std::string cppstring;
public:
    rule_of_zero(const std::string& arg) : cppstring(arg) {}
};
**Note:** Le code C++ n'a pas été traduit conformément aux instructions, car il se trouve dans des balises `
` et contient des termes spécifiques au C++. Seul le texte environnant aurait été traduit s'il y en avait eu.

Lorsqu'une classe de base est destinée à être utilisée de manière polymorphe, son destructeur doit être déclaré public et virtual . Cela bloque les déplacements implicites (et rend obsolètes les copies implicites), et donc les fonctions membres spéciales doivent être définies comme = default [2] .

class base_of_five_defaults
{
public:
    base_of_five_defaults(const base_of_five_defaults&) = default;
    base_of_five_defaults(base_of_five_defaults&&) = default;
    base_of_five_defaults& operator=(const base_of_five_defaults&) = default;
    base_of_five_defaults& operator=(base_of_five_defaults&&) = default;
    virtual ~base_of_five_defaults() = default;
};

Cependant, cela rend la classe sujette au slicing, c'est pourquoi les classes polymorphes définissent souvent la copie comme = delete (voir C.67 : Une classe polymorphe doit supprimer la copie/déplacement publique dans les C++ Core Guidelines), ce qui conduit à la formulation générique suivante pour la Règle des Cinq :

C.21 : Si vous définissez ou =delete toute fonction de copie, de déplacement ou de destructeur, définissez ou =delete-les toutes.

Liens externes

  1. "Règle de Zéro", R. Martinho Fernandes 15/08/2012
  2. "Une préoccupation concernant la Règle de Zéro", Scott Meyers, 13/03/2014 .