1 votes

La règle des 5 (pour les constructeurs et les destructeurs) est-elle dépassée ?

La règle des 5 stipule que si une classe possède un destructeur, un constructeur de copie, un constructeur d'affectation de copie, un constructeur de déplacement ou un constructeur d'affectation de déplacement déclarés par l'utilisateur, alors elle doit posséder les 4 autres.

Mais aujourd'hui, je me suis rendu compte que je n'ai jamais eu besoin d'un destructeur, d'un constructeur de copie, d'un constructeur d'affectation de copie, d'un constructeur de déplacement ou d'un constructeur d'affectation de déplacement définis par l'utilisateur.

D'après moi, les constructeurs et destructeurs implicites fonctionnent parfaitement pour les structures de données agrégées. Cependant, les classes qui gèrent une ressource ont besoin de constructeurs/destructeurs définis par l'utilisateur.

Cependant, toutes les classes de gestion des ressources ne peuvent-elles pas être converties en une structure de données agrégée utilisant un pointeur intelligent ?

Exemple :

// RAII Class which allocates memory on the heap.
class ResourceManager {
    Resource* resource;
    ResourceManager() {resource = new Resource;}
    // In this class you need all the destructors/ copy ctor/ move ctor etc...
    // I haven't written them as they are trivial to implement
};

vs

class ResourceManager {
    std::unique_ptr<Resource> resource;
};

Maintenant, l'exemple 2 se comporte exactement comme l'exemple 1, mais tous les constructeurs implicites fonctionnent.

Bien sûr, vous ne pouvez pas copier ResourceManager mais si vous souhaitez un comportement différent, vous pouvez utiliser un autre pointeur intelligent.

Le fait est que vous n'avez pas besoin de constructeurs définis par l'utilisateur lorsque les pointeurs intelligents en ont déjà, de sorte que les constructeurs implicites fonctionnent.

La seule raison que je verrais pour avoir des constructeurs définis par l'utilisateur serait quand :

  1. vous ne pouvez pas utiliser de pointeurs intelligents dans du code de bas niveau (je doute fortement que ce soit jamais le cas).

  2. vous implémentez les pointeurs intelligents eux-mêmes.

Cependant, dans un code normal, je ne vois aucune raison d'utiliser des constructeurs définis par l'utilisateur.

Est-ce que je rate quelque chose ici ?

63voto

HolyBlackCat Points 2137

Le nom complet de la règle est la règle des 3/5/0 .

Il n'a pas dit "toujours fournir les cinq". Il est dit que vous devez soit fournir les trois, les cinq, ou aucun d'entre eux.

En effet, le plus souvent, la meilleure solution consiste à ne fournir aucun des cinq éléments. Mais vous ne pouvez pas le faire si vous écrivez votre propre conteneur, un pointeur intelligent ou un wrapper RAII autour d'une ressource.

17voto

Jarod42 Points 15729

Cependant, dans un code normal, je ne vois aucune raison d'utiliser des constructeurs définis par l'utilisateur.

Le constructeur fourni par l'utilisateur permet également de maintenir un certain invariant, donc orthogonal à la règle des 5.

Comme par exemple un

struct clampInt
{
    int min;
    int max;
    int value;
};

ne garantit pas que min < max . Ainsi, encapsuler les données pourrait fournir cette garantie. L'agrégat ne convient pas à tous les cas.

quand avez-vous besoin d'un destru destru destructeur, d'un constructeur de copie, d'un constructeur d'affectation de copie, d'un constructeur de déplacement ou d'un constructeur d'affectation de déplacement définis par l'utilisateur ?

Maintenant, à propos de la règle du 5/3/0.

En effet, la règle du 0 doit être privilégiée.

Les pointeurs intelligents disponibles (j'inclus le conteneur) sont pour les pointeurs, les collections ou les Verrouillables . Mais les ressources ne sont pas nécessairement des pointeurs (elles peuvent être poignée caché dans un int les variables statiques internes cachées ( XXX_Init() / XXX_Close() )), ou peut nécessiter un traitement plus avancé (comme pour la base de données, un commit automatique à la fin de la portée ou un retour en arrière en cas d'exceptions), vous devez donc écrire votre propre objet RAII.

Vous pouvez également écrire un objet RAII qui ne possède pas vraiment de ressource, comme un objet de type TimerLogger par exemple (écrire le temps écoulé utilisé par un "scope").

Un autre moment où vous devez généralement écrire un destructeur est pour les classes abstraites, car vous avez besoin d'un destructeur virtuel (et une éventuelle copie polymorphe est effectuée par un destructeur virtuel). clone ).

12voto

Yakk Points 31636

La règle complète est, comme indiqué, la règle des 0/3/5 ; mettez en œuvre 0 d'entre elles en général, et si vous en mettez une, mettez en œuvre 3 ou 5 d'entre elles.

Vous devez mettre en œuvre les opérations de copie/déplacement et de destruction dans quelques cas.

  1. Auto-référence. Parfois, certaines parties d'un objet font référence à d'autres parties de l'objet. Lorsque vous les copiez, elles font naïvement référence à l'objet autre l'objet dont vous avez fait la copie.

  2. Des pointeurs intelligents. Il y a des raisons de mettre en œuvre davantage de pointeurs intelligents.

  3. Plus généralement que les pointeurs intelligents, les types propriétaires de ressources, comme les vector ou optional o variant s. Ce sont tous des types de vocabulaire qui permettent à leurs utilisateurs de ne pas s'en soucier.

  4. Plus généraux que 1, les objets dont l'identité importe. Les objets qui ont un enregistrement externe, par exemple, doivent réenregistrer la nouvelle copie auprès du magasin d'enregistrement, et lorsqu'ils sont détruits, ils doivent se désenregistrer eux-mêmes.

  5. Les cas où vous devez faire preuve de prudence ou de fantaisie en raison de la concurrence. Par exemple, si vous avez un mutex_guarded<T> et vous voulez qu'ils soient copiables, la copie par défaut ne fonctionne pas car le wrapper a un mutex, et les mutex ne peuvent pas être copiés. Dans d'autres cas, vous pouvez avoir besoin de garantir l'ordre de certaines opérations, de faire des comparaisons et des ensembles, ou même de suivre ou d'enregistrer le "thread natif" de l'objet pour détecter quand il a franchi les limites du thread.

6voto

JVApen Points 4523

Disposer de bons concepts encapsulés qui suivent déjà la règle des cinq garantit en effet que vous devez moins vous en préoccuper. Cela dit, si vous vous trouvez dans une situation où vous devez écrire une logique personnalisée, la règle est toujours valable. Quelques exemples qui me viennent à l'esprit :

  • Vos propres types de pointeurs intelligents
  • Les observateurs qui doivent se désinscrire
  • Wrappers pour les bibliothèques C

En outre, je trouve qu'une fois que vous avez suffisamment de composition, le comportement d'une classe n'est plus clair. Les opérateurs d'affectation sont-ils disponibles ? Peut-on copier la construction de la classe ? Par conséquent, en appliquant la règle des cinq, même avec = default dans celui-ci, en combinaison avec -Wdefaulted-function-deleted car les erreurs aident à comprendre le code.

Pour regarder de plus près vos exemples :

// RAII Class which allocates memory on the heap.
class ResourceManager {
    Resource* resource;
    ResourceManager() {resource = new Resource;}
    // In this class you need all the destructors/ copy ctor/ move ctor etc...
    // I haven't written them as they are trivial to implement
};

Ce code pourrait en effet être joliment converti en :

class ResourceManager {
    std::unique_ptr<Resource> resource;
};

Cependant, imaginez maintenant :

class ResourceManager {
    ResourcePool &pool;
    Resource *resource;

    ResourceManager(ResourcePool &pool) : pool{pool}, resource{pool.createResource()} {}
    ~ResourceManager() { pool.destroyResource(resource);
};

Encore une fois, cela pourrait être fait avec un unique_ptr si vous lui donnez un destructeur personnalisé. Cependant, si votre classe stocke maintenant beaucoup de ressources, êtes-vous prêt à payer le coût supplémentaire en mémoire ?

Que se passe-t-il si vous devez d'abord prendre un verrou avant de pouvoir remettre la ressource dans le pool pour la recycler ? Prendrez-vous ce verrou une seule fois et rendre toutes les ressources ou 1000 fois en les rendant 1 par 1 ?

Je pense que votre raisonnement est correct, avoir de bons types de pointeurs intelligents rend la règle des 5 moins pertinente. Cependant, comme indiqué dans cette réponse, il y a toujours des cas à découvrir où vous en aurez besoin. La qualifier de dépassée est peut-être un peu exagéré, c'est un peu comme savoir comment itérer avec for (auto it = v.begin(); it != v.end(); ++it) au lieu de for (auto e : v) . Vous n'utilisez plus la première variante, jusqu'au moment où vous devez l'appeler "effacer" et où elle redevient soudainement pertinente.

5voto

bolov Points 4005

La règle est souvent mal comprise car elle est souvent trouvée trop simplifiée.

El simplifié est la suivante : si vous devez écrire au moins une des (3/5) méthodes spéciales, vous devez écrire toutes les (3/5).

Le réel, utile règle : Une classe qui est responsable de la propriété manuelle d'une ressource doit : s'occuper exclusivement de la gestion de la propriété/de la durée de vie de la ressource ; pour le faire correctement, elle doit implémenter les 3/5 membres spéciaux. Sinon (si votre classe n'a pas la propriété manuelle d'une ressource) vous devez laisser tous les membres spéciaux implicites ou par défaut (règle du zéro).

Les versions simplifiées utilisent cette rhétorique : si vous vous trouvez dans la nécessité d'écrire l'un des (3/5), alors il est fort probable que votre classe gère manuellement la propriété d'une ressource et vous devez donc implémenter tous les (3/5).

Exemple 1 : si votre classe gère l'acquisition/la libération d'une ressource système, elle doit implémenter les 3/5.

Exemple 2 : si votre classe gère la durée de vie d'une région mémoire, elle doit implémenter les 3/5.

Exemple 3 : dans votre destructeur vous faites de la journalisation. La raison pour laquelle vous écrivez un destructeur n'est pas pour gérer une ressource que vous possédez, donc vous n'avez pas besoin d'écrire les autres membres spéciaux.

En conclusion : dans le code utilisateur, vous devez suivre la règle du zéro : ne pas gérer manuellement les ressources. Utilisez les wrappers RAII qui implémentent déjà cela pour vous (comme les pointeurs intelligents, les conteneurs standard, std::string etc.)

Cependant, si vous avez besoin de gérer manuellement une ressource, écrivez une classe RAII qui sera exclusivement responsable de la gestion de la durée de vie de la ressource. Cette classe doit implémenter tous les membres spéciaux (3/5).

Une bonne lecture à ce sujet : https://en.cppreference.com/w/cpp/language/rule_of_three

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X