Recherche…


Destructeurs virtuels et protégés

Une classe conçue pour être héritée de s'appelle une classe de base. Des précautions doivent être prises avec les fonctions membres spéciales d'une telle classe.

Une classe conçue pour être utilisée de manière polymorphe à l'exécution (via un pointeur sur la classe de base) doit déclarer le destructeur virtual . Cela permet aux parties dérivées de l'objet d'être correctement détruites, même lorsque l'objet est détruit via un pointeur vers la classe de base.

class Base {
public:
    virtual ~Base() = default;

private:
    //    data members etc.
};

class Derived : public Base { //  models Is-A relationship
public:
    //    some methods

private:
    //    more data members
};

//    virtual destructor in Base ensures that derived destructors
//    are also called when the object is destroyed
std::unique_ptr<Base> base = std::make_unique<Derived>();
base = nullptr;  //    safe, doesn't leak Derived's members

Si la classe n'a pas besoin d'être polymorphe, mais doit toujours permettre à son interface d'être héritée, utilisez un destructeur protected non virtuel.

class NonPolymorphicBase {
public:
    //    some methods

protected:
    ~NonPolymorphicBase() = default; //    note: non-virtual

private:
    //    etc.
};

Une telle classe ne peut jamais être détruite par un pointeur, évitant les fuites silencieuses dues au découpage.

Cette technique s'applique particulièrement aux classes conçues pour être private classes de base private . Une telle classe pourrait être utilisée pour encapsuler certains détails d'implémentation courants, tout en fournissant virtual méthodes virtual tant que points de personnalisation. Ce type de classe ne devrait jamais être utilisé de manière polymorphe, et un destructeur protected aide à documenter cette exigence directement dans le code.

Enfin, certaines classes peuvent exiger qu’elles ne soient jamais utilisées comme classe de base. Dans ce cas, la classe peut être marquée comme final . Un destructeur public non virtuel normal est correct dans ce cas.

class FinalClass final {  //    marked final here
public:
    ~FinalClass() = default;

private:
    //    etc.
};

Déplacement et copie implicites

Gardez à l'esprit que la déclaration d'un destructeur empêche le compilateur de générer des constructeurs de déplacements implicites et de déplacer des opérateurs d'affectation. Si vous déclarez un destructeur, n'oubliez pas d'ajouter également les définitions appropriées pour les opérations de déplacement.

De plus, la déclaration des opérations de déplacement supprime la génération des opérations de copie, elles doivent donc être ajoutées (si les objets de cette classe doivent avoir une sémantique de copie).

class Movable {
public:
    virtual ~Movable() noexcept = default;

    //    compiler won't generate these unless we tell it to
    //    because we declared a destructor
    Movable(Movable&&) noexcept = default;
    Movable& operator=(Movable&&) noexcept = default;

    //    declaring move operations will suppress generation
    //    of copy operations unless we explicitly re-enable them
    Movable(const Movable&) = default;
    Movable& operator=(const Movable&) = default;
};

Copier et échanger

Si vous écrivez une classe qui gère des ressources, vous devez implémenter toutes les fonctions membres spéciales (voir Règle de trois / cinq / zéro ). L'approche la plus directe pour écrire le constructeur de copie et l'opérateur d'assignation serait:

person(const person &other)
    : name(new char[std::strlen(other.name) + 1])
    , age(other.age)
{
    std::strcpy(name, other.name);
}

person& operator=(person const& rhs) {
    if (this != &other) {
        delete [] name;
        name = new char[std::strlen(other.name) + 1];
        std::strcpy(name, other.name);
        age = other.age;
    }

    return *this;
}

Mais cette approche a quelques problèmes. Il ne la garantie forte d'exception - si new[] lancers francs, nous avons déjà éclairci les ressources appartenant à this et ne peut pas récupérer. Nous dupliquons une grande partie de la logique de la construction de copies dans l’affectation de copies. Et nous devons nous souvenir de la vérification de l’auto-assignation, qui ne fait généralement qu’ajouter de la charge à l’opération de copie, mais qui reste essentielle.

Pour satisfaire la garantie d'exception forte et éviter la duplication de code (doubler avec l'opérateur d'affectation de mouvement suivant), nous pouvons utiliser l'idiome de copie et d'échange:

class person {
    char* name;
    int age;
public:
    /* all the other functions ... */

    friend void swap(person& lhs, person& rhs) {
        using std::swap; // enable ADL

        swap(lhs.name, rhs.name);
        swap(lhs.age, rhs.age);
    }

    person& operator=(person rhs) {
        swap(*this, rhs);
        return *this;
    }
};

Pourquoi ça marche? Considérez ce qui se passe quand nous avons

person p1 = ...;
person p2 = ...;
p1 = p2;

Tout d'abord, nous copions- rhs partir de p2 (que nous n'avons pas eu à dupliquer ici). Si cette opération est lancée, nous ne faisons rien dans operator= et p1 reste intact. Ensuite, nous échangeons les membres entre *this et rhs , puis rhs sort de la portée. Lorsque l' operator= , qui nettoie implicitement les ressources d' origine de this (via le destructor, que nous ne devions pas faire double emploi). L'auto-assignation fonctionne aussi - elle est moins efficace avec les opérations de copie-échange (qui impliquent une allocation et une désallocation supplémentaires), mais si c'est le cas peu probable, nous ne ralentissons pas le cas d'utilisation type pour le prendre en compte.

C ++ 11

La formulation ci-dessus fonctionne comme si elle était déjà utilisée pour une affectation de déplacement.

p1 = std::move(p2);

Ici, on déplace-construit rhs partir de p2 , et tout le reste est tout aussi valide. Si une classe est déplaçable mais non copiable, il n'est pas nécessaire de supprimer l'assignation de copie, car cet opérateur d'affectation sera simplement mal formé en raison du constructeur de copie supprimé.

Constructeur par défaut

Un constructeur par défaut est un type de constructeur qui ne nécessite aucun paramètre lorsqu'il est appelé. Il est nommé d'après le type qu'il construit et en est une fonction membre (comme tous les constructeurs).

class C{
    int i;
public:
    // the default constructor definition
    C()
    : i(0){ // member initializer list -- initialize i to 0
        // constructor function body -- can do more complex things here
    }
};

C c1; // calls default constructor of C to create object c1
C c2 = C(); // calls default constructor explicitly
C c3(); // ERROR: this intuitive version is not possible due to "most vexing parse"
C c4{}; // but in C++11 {} CAN be used in a similar way

C c5[2]; // calls default constructor for both array elements
C* c6 = new C[2]; // calls default constructor for both array elements

Une autre façon de satisfaire à l'exigence "pas de paramètres" est que le développeur fournisse des valeurs par défaut pour tous les paramètres:

class D{
    int i;
    int j;
public:
    // also a default constructor (can be called with no parameters)
    D( int i = 0, int j = 42 ) 
    : i(i), j(j){
    }
};


D d; // calls constructor of D with the provided default values for the parameters

Dans certaines circonstances (c'est-à-dire que le développeur ne fournit aucun constructeur et qu'il n'y a pas d'autres conditions de disqualification), le compilateur fournit implicitement un constructeur par défaut vide:

class C{
    std::string s; // note: members need to be default constructible themselves
};

C c1; // will succeed -- C has an implicitly defined default constructor

Avoir un autre type de constructeur est l’une des conditions de disqualification mentionnées précédemment:

class C{
    int i;
public:
    C( int i ) : i(i){}
};

C c1; // Compile ERROR: C has no (implicitly defined) default constructor
c ++ 11

Pour empêcher la création implicite de constructeur par défaut, une technique courante consiste à la déclarer private (sans définition). L'intention est de provoquer une erreur de compilation lorsque quelqu'un essaie d'utiliser le constructeur (cela entraîne un accès à une erreur privée ou une erreur de l'éditeur de liens, selon le compilateur).

Pour être sûr qu'un constructeur par défaut (fonctionnellement similaire à l'implicite) est défini, un développeur pourrait en écrire un explicitement vide.

c ++ 11

En C ++ 11, un développeur peut également utiliser le mot-clé delete pour empêcher le compilateur de fournir un constructeur par défaut.

class C{
    int i;
public:
    // default constructor is explicitly deleted
    C() = delete;
};

C c1; // Compile ERROR: C has its default constructor deleted

En outre, un développeur peut également être explicite quant à la volonté du compilateur de fournir un constructeur par défaut.

class C{
    int i;
public:
    // does have automatically generated default constructor (same as implicit one)
    C() = default;

    C( int i ) : i(i){}
};

C c1; // default constructed
C c2( 1 ); // constructed with the int taking constructor 
c ++ 14

Vous pouvez déterminer si un type a un constructeur par défaut (ou est un type primitif) en utilisant std::is_default_constructible partir de <type_traits> :

class C1{ };
class C2{ public: C2(){} };
class C3{ public: C3(int){} };

using std::cout; using std::boolalpha; using std::endl;
using std::is_default_constructible;
cout << boolalpha << is_default_constructible<int>() << endl; // prints true
cout << boolalpha << is_default_constructible<C1>() << endl; // prints true
cout << boolalpha << is_default_constructible<C2>() << endl; // prints true
cout << boolalpha << is_default_constructible<C3>() << endl; // prints false
c ++ 11

En C ++ 11, il est toujours possible d'utiliser la version non-foncteur de std::is_default_constructible :

cout << boolalpha << is_default_constructible<C1>::value << endl; // prints true

Destructeur

Un destructeur est une fonction sans arguments appelée lorsqu'un objet défini par l'utilisateur est sur le point d'être détruit. Il est nommé d'après le type qu'il détruit avec un préfixe ~ .

class C{
    int* is;
    string s;
public:
    C()
    : is( new int[10] ){
    }

    ~C(){  // destructor definition
        delete[] is;
    }
};

class C_child : public C{
    string s_ch;
public:
    C_child(){}
    ~C_child(){} // child destructor
};

void f(){
    C c1; // calls default constructor
    C c2[2]; // calls default constructor for both elements
    C* c3 = new C[2]; // calls default constructor for both array elements

    C_child c_ch;  // when destructed calls destructor of s_ch and of C base (and in turn s)

    delete[] c3; // calls destructors on c3[0] and c3[1]
} // automatic variables are destroyed here -- i.e. c1, c2 and c_ch

Dans la plupart des cas (c.-à-d. Qu'un utilisateur ne fournit aucun destructeur et qu'il n'y a pas d'autres conditions disqualifiantes), le compilateur fournit implicitement un destructeur par défaut:

class C{
    int i;
    string s;
};

void f(){
    C* c1 = new C;
    delete c1; // C has a destructor
}

class C{
    int m;
private:
    ~C(){} // not public destructor!
};

class C_container{
    C c;
};

void f(){
    C_container* c_cont = new C_container;
    delete c_cont; // Compile ERROR: C has no accessible destructor
}
c ++ 11

En C ++ 11, un développeur peut remplacer ce comportement en empêchant le compilateur de fournir un destructeur par défaut.

class C{
    int m;
public:
    ~C() = delete; // does NOT have implicit destructor
};

void f{
    C c1; 
} // Compile ERROR: C has no destructor

En outre, un développeur peut également être explicite quant à la volonté du compilateur de fournir un destructeur par défaut.

class C{
    int m;
public:
    ~C() = default; // saying explicitly it does have implicit/empty destructor
};

void f(){
    C c1;
} // C has a destructor -- c1 properly destroyed
c ++ 11

Vous pouvez déterminer si un type a un destructeur (ou est un type primitif) en utilisant std::is_destructible de <type_traits> :

class C1{ };
class C2{ public: ~C2() = delete };
class C3 : public C2{ };

using std::cout; using std::boolalpha; using std::endl;
using std::is_destructible;
cout << boolalpha << is_destructible<int>() << endl; // prints true
cout << boolalpha << is_destructible<C1>() << endl; // prints true
cout << boolalpha << is_destructible<C2>() << endl; // prints false
cout << boolalpha << is_destructible<C3>() << endl; // prints false


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow