La règle du 5 - l'utiliser ou non?

20

La règle de 3 ( la règle de 5 dans la nouvelle norme c ++) stipule:

Si vous devez déclarer vous-même explicitement le destructeur, le constructeur de copie ou l'opérateur d'affectation de copie, vous devez probablement déclarer explicitement les trois.

Mais, d'autre part, le " Clean Code " de Martin conseille de supprimer tous les constructeurs et destructeurs vides (page 293, G12: Clutter ):

À quoi sert un constructeur par défaut sans implémentation? Tout ce qu'il sert à faire est d'encombrer le code avec des artefacts sans signification.

Alors, comment gérer ces deux opinions opposées? Faut-il vraiment implémenter des constructeurs / destructeurs vides?


L'exemple suivant montre exactement ce que je veux dire:

#include <iostream>
#include <memory>

struct A
{
    A( const int value ) : v( new int( value ) ) {}
    ~A(){}
    A( const A & other ) : v( new int( *other.v ) ) {}
    A& operator=( const A & other )
    {
        v.reset( new int( *other.v ) );
        return *this;
    }

    std::auto_ptr< int > v;
};
int main()
{
    const A a( 55 );
    std::cout<< "a value = " << *a.v << std::endl;
    A b(a);
    std::cout<< "b value = " << *b.v << std::endl;
    const A c(11);
    std::cout<< "c value = " << *c.v << std::endl;
    b = c;
    std::cout<< "b new value = " << *b.v << std::endl;
}

Compile bien en utilisant g ++ 4.6.1 avec:

g++ -std=c++0x -Wall -Wextra -pedantic example.cpp

Le destructeur pour struct Aest vide et n'est pas vraiment nécessaire. Alors, devrait-il être là ou devrait-il être supprimé?

BЈовић
la source
15
Les 2 citations parlent de choses différentes. Ou je manque totalement votre point.
Benjamin Bannier
1
@honk Dans la norme de codage de mon équipe, nous avons une règle pour toujours déclarer les 4 (constructeur, destructeur, copier des constructeurs). Je me demandais si cela avait vraiment du sens. Dois-je vraiment toujours déclarer des destructeurs, même s'ils sont vides?
BЈовић
En ce qui concerne les destructeurs vides, pensez-y: codesynthesis.com/~boris/blog/2012/04/04/… . Sinon, la règle de 3 (5) est parfaitement logique pour moi, aucune idée pourquoi on voudrait une règle de 4.
Benjamin Bannier
@honk Méfiez-vous des informations que vous trouvez sur le net. Tout n'est pas vrai. Par exemple, virtual ~base () = default;ne compile pas (pour une bonne raison)
BЈовић
@VJovic, Non, vous n'avez pas à déclarer un destructeur vide, sauf si vous devez le rendre virtuel. Et pendant que nous sommes sur le sujet, vous ne devriez pas utiliser auto_ptrnon plus.
Dima

Réponses:

44

Pour commencer, la règle dit "probablement", donc elle ne s'applique pas toujours.

Le deuxième point que je vois ici est que si vous devez déclarer l'un des trois, c'est parce qu'il fait quelque chose de spécial comme l'allocation de mémoire. Dans ce cas, les autres ne seraient pas vides car ils devraient gérer la même tâche (comme copier le contenu de la mémoire allouée dynamiquement dans le constructeur de copie ou libérer une telle mémoire).

Donc, en conclusion, vous ne devez pas déclarer de constructeurs ou de destructeurs vides, mais il est très probable que si l'un est nécessaire, les autres le sont aussi.

Comme pour votre exemple: dans un tel cas, vous pouvez laisser le destructeur hors tension. Cela ne fait rien, évidemment. L'utilisation de pointeurs intelligents est un exemple parfait d'où et pourquoi la règle de 3 ne tient pas.

C'est juste un guide pour savoir où jeter un second regard sur votre code au cas où vous auriez oublié de mettre en œuvre des fonctionnalités importantes que vous auriez autrement pu manquer.

thorsten müller
la source
Avec l'utilisation de pointeurs intelligents, les destructeurs sont vides dans la plupart des cas (je dirais que> 99% des destructeurs dans ma base de code sont vides, car presque toutes les classes utilisent l'idiome pimpl).
BЈовић
Wow, c'est tellement de boutons que j'appellerais ça puant. Avec de nombreux compilateurs, les boutons sont plus difficiles à optimiser (par exemple, plus difficiles à aligner).
Benjamin Bannier
@honk Qu'entendez-vous par "de nombreux compilateurs maculés"? :)
BЈовић
@VJovic: désolé, faute de frappe: 'code pimpled'
Benjamin Bannier
4

Il n'y a vraiment aucune contradiction ici. La règle de 3 parle du destructeur, du constructeur de copie et de l'opérateur d'affectation de copie. L'oncle Bob parle de constructeurs par défaut vides.

Si vous avez besoin d'un destructeur, alors votre classe contient probablement des pointeurs vers la mémoire allouée dynamiquement, et vous voulez probablement avoir un ctor de copie et un operator=()qui font une copie complète . Ceci est complètement orthogonal à la nécessité ou non d'un constructeur par défaut.

Notez également qu'en C ++, il existe des situations où vous avez besoin d'un constructeur par défaut, même s'il est vide. Supposons que votre classe ait un constructeur non par défaut. Dans ce cas, le compilateur ne générera pas de constructeur par défaut pour vous. Cela signifie que les objets de cette classe ne peuvent pas être stockés dans des conteneurs STL, car ces conteneurs s'attendent à ce que les objets soient constructibles par défaut.

D'un autre côté, si vous ne prévoyez jamais de mettre les objets de votre classe dans des conteneurs STL, un constructeur par défaut vide est certainement inutile.

Dima
la source
2

Ici, votre équivalent potentiel (*) au constructeur / affectation / destructeur par défaut a un but: documenter le fait que vous avez bien réfléchi au problème et déterminé que le comportement par défaut était correct. BTW, en C ++ 11, les choses ne se sont pas suffisamment stabilisées pour savoir si elles =defaultpeuvent servir cet objectif.

(Il existe un autre objectif potentiel: fournir une définition hors ligne au lieu de la définition en ligne par défaut, mieux vaut documenter explicitement si vous avez une raison de le faire).

(*) Potentiel car je ne me souviens pas d'un cas réel où la règle de trois ne s'applique pas, si je devais faire quelque chose dans l'un, je devais faire quelque chose dans les autres.


Modifiez après l'ajout d'un exemple. votre exemple utilisant auto_ptr est intéressant. Vous utilisez un pointeur intelligent, mais pas celui qui est à la hauteur de la tâche. Je préfère en écrire un qui est - surtout si la situation se produit souvent - plutôt que de faire ce que vous avez fait. (Si je ne me trompe pas, ni le standard ni le boost n'en fournissent un).

AProgrammer
la source
L'exemple démontre mon point. Le destructeur n'est pas vraiment nécessaire, mais la règle de 3 dit qu'il devrait être là.
BЈовић
1

La règle de 5 est une extension prudente de la règle de 3 qui est un comportement prudent contre une mauvaise utilisation possible d'un objet.

Si vous avez besoin d'un destructeur, cela signifie que vous avez fait une "gestion des ressources" autre que celle par défaut (il suffit de construire et de détruire valeurs ).

Depuis copier, assigner, déplacer et transférer par défaut les valeurs de copie , si vous ne tenez pas juste valeurs , vous devez définir ce qu'il faut faire.

Cela dit, C ++ supprime la copie si vous définissez le déplacement et supprime le déplacement si vous définissez la copie. Dans la plupart des cas, vous devez définir si vous souhaitez émuler une valeur (d'où copier copier clone la ressource et déplacer n'a aucun sens) ou un gestionnaire de ressources (et donc déplacer la ressource, où copie n'a aucun sens: la règle de 3 devient la règle des 3 autres )

Les cas où vous devez définir à la fois copier et déplacer (règle de 5) sont assez rares: vous avez généralement une "grande valeur" qui doit être copiée si elle est donnée à des objets distincts, mais peut être déplacée si elle est prise à partir d'un objet temporaire (en évitant un clone puis détruisez ). C'est le cas des conteneurs STL ou des conteneurs arithmétiques.

Un cas peut être des matrices: elles doivent prendre en charge la copie car ce sont des valeurs, ( a=b; c=b; a*=2; b*=3;ne doivent pas s'influencer mutuellement) mais elles peuvent être optimisées en prenant également en charge le déplacement ( a = 3*b+4*ca un +qui prend deux temporaires et génère un temporaire: éviter le clonage et la suppression peut être utile)

Emilio Garavaglia
la source
1

Je préfère une formulation différente de la règle des trois, qui semble plus raisonnable, qui est "si votre classe a besoin d'un destructeur (autre qu'un destructeur virtuel vide), elle a probablement aussi besoin d'un constructeur de copie et d'un opérateur d'affectation."

Le spécifier comme une relation à sens unique à partir du destructeur rend certaines choses plus claires:

  1. Il ne s'applique pas dans les cas où vous fournissez un constructeur de copie ou un opérateur d'affectation non par défaut uniquement à titre d'optimisation.

  2. La raison de la règle est que le constructeur de copie ou l'opérateur d'affectation par défaut peut bousiller la gestion manuelle des ressources. Si vous gérez manuellement des ressources, vous avez probablement réalisé que vous aurez besoin d'un destructeur pour les libérer.

Jules
la source
-3

Il y a un autre point qui n'est pas encore mentionné dans la discussion: un destructeur doit toujours être virtuel.

struct A
{
    A( const int value ) : v( new int( value ) ) {}
    virtual ~A(){}
    ...
}

Le constructeur doit être déclaré virtuel dans la classe de base pour le rendre également virtuel dans toutes les classes dérivées. Donc, même si votre classe de base n'a pas besoin d'un destructeur, vous finissez par déclarer et implémenter un destructeur vide.

Si vous mettez tous les avertissements sur (-Wall -Wextra -Weffc ++) g ++ vous en avertira. Je considère comme une bonne pratique de toujours déclarer un destructeur virtuel dans n'importe quelle classe, car on ne sait jamais si votre classe deviendra éventuellement une classe de base. Si le destructeur virtuel n'est pas nécessaire, il ne fait aucun mal. Si c'est le cas, vous gagnez du temps pour trouver l'erreur.

Lexi
la source
1
Mais je ne veux pas du constructeur virtuel. Si je fais cela, alors chaque appel à une méthode utiliserait la répartition virtuelle. btw prend note qu'il n'existe pas de "constructeur virtuel" en c ++. De plus, j'ai compilé l'exemple avec un niveau d'avertissement très élevé.
BЈовић
IIRC, la règle que gcc utilise pour son avertissement, et la règle que je respecte généralement de toute façon, est qu'il devrait y avoir un destructeur virtuel s'il y a d'autres méthodes virtuelles dans la classe.
Jules