40
votes

La règle de 5 (pour les constructeurs et destructeurs) est-elle obsolète?

La règle de 5 stipule que si une classe a un destructeur, un constructeur de copie, un constructeur d'attribution de copie, un constructeur de déplace MOI: Quand avez-vous déjà besoin d'un destructeur, de copie-copieur, de copie de copie, constructeur, constructeur de déplacements ou constructeur d'attribution de déménagement?

Dans ma compréhension, les constructeurs / destructeurs implicites fonctionnent très bien pour les structures de données agrégées . Cependant, les classes qui gèrent une ressource nécessitent des 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 à l'aide d'un pointeur intelligent?

Exemple:

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

vs

// 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
};

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 voulez un comportement différent, vous pouvez utiliser un pointeur intelligent différent.

Est-ce que vous n'avez pas besoin de constructeurs définis par l'utilisateur lorsque les pointeurs intelligents ont déjà ces constructeurs si implicites.

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

  • Vous ne pouvez pas utiliser les pointeurs intelligents dans un code de bas niveau (je doute fortement que ce soit jamais le cas).

  • Vous implémentez eux-mêmes les pointeurs intelligents.

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

    Sois-je manquer quelque chose ici?


    19 commentaires

    Si chaque instance de la classe gère sa propre instance de la ressource et qu'il est également nécessaire de copier (ou de déplacer) cette ressource entre les instances de la classe, alors la règle de cinq s'applique. Si votre classe délègue que la copie / le passage à un pointeur intelligent approprié (qui, à son tour, doit gérer la copie / le déplacement de la ressource, et donc se conformer à la règle de cinq), votre classe peut se conformer à la règle de zéro.


    @Peter c'est mon point. Pourquoi ne pouvez-vous pas toujours déléguer le déplacement / la copie sur un pointeur intelligent?


    Et si vous écrivez votre propre pointeur intelligent?


    C'est ce qu'on appelle "la règle de zéro".


    @Galik donc la règle de 5 est obsolète? Vous devriez suivre la règle de zéro?


    @Cyrus non dépassé mais la règle de zéro est préférée. La règle de 3/5 s'applique toujours lorsque vous avez un (s) membre qui ne gère pas ses propres ressources.


    @Galik Je ne comprends pas quand vous auriez un membre qui ne gère pas ses propres ressources. Normalement, vous auriez un pointeur intelligent en prenant soin de cela. Pourriez-vous m'en donner un exemple s'il vous plaît (à part le cas où vous implémentez un pointeur intelligent)?


    Encore une fois, supposons que vous écrivez votre propre pointeur intelligent à partir de zéro.


    Vous pouvez avoir une poignée de fichier de niveau de système d'exploitation brut à gérer (par exemple).


    Qu'en est-il de la mise en œuvre de presque tous les conteneurs? Si vous implémentez un vecteur, vous devez allouer (et donc traiter) la mémoire. Ici, la règle de 3/5 est cruciale. Gardez à l'esprit que tout le monde n'utilise pas de conteneurs STL. Si vous pouvez exprimer votre programme exclusivement en composant d'autres structures de données, vous n'avez pas à implémenter aucun des cinq et la règle de 0 s'applique.


    Tout ce qui a une sémantique inhabituelle à acquérir / libérer.


    Votre exemple est tout simplement un peu artificiel pour faire valoir le point. Mais ce n'est pas bon. Disons que votre constructeur crée un nouveau tableau dans une base de données, que le destructeur doit finaliser. Comment éviteriez-vous cela avec un pointeur intelligent?


    @Tasospapastylianou ne peut pas std :: unique_ptr de prendre un déleter personnalisé comme argument de modèle?


    @Cyrus peut-être, mais je considère que cela est de déléguer des fonctionnalités qui appartiennent logiquement aux responsabilités de votre classe à une entité externe non apparentée juste pour éviter la règle.


    @ Tasospapastylianou assez vrai. Je suppose que certains cas ont besoin de RAII au lieu de pointeurs intelligents ... mais si j'utilise des pointeurs pour la gestion des ressources, j'utiliserais certainement des pointeurs intelligents.


    Les délecteurs personnalisés ne vont que si loin. Et, pour être honnête, utiliser un pointeur pour gérer quelque chose qui n'est pas un pointeur est trompeur, donc je ne le recommanderais pas. Je fais beaucoup de délèves personnalisés comme vous le suggérez quand cela a du sens. Mais pour d'autres choses, j'écris un gestionnaire de ressources dédié (en utilisant la règle ou 3/5) afin qu'à l'avenir, je puisse utiliser ce gestionnaire de ressources dans d'autres classes qui suivent la règle de zéro.


    @Cyrus - Vous supposez, à tort, qu'un type de point intelligent existant convient pour gérer toutes les ressources que vous pourriez avoir besoin pour gérer. Les pointeurs intelligents de la bibliothèque standard C ++ gèrent des types particuliers de ressources, mais pas d'autres. Si vous avez besoin de gérer certaines ressources autres (c'est-à-dire que la bibliothèque standard C ++ ne fournit pas de classe appropriée pour le gérer), vous rédigerez votre propre gestionnaire. Il existe de nombreux exemples de ressources qui ne sont pas prises en charge par la bibliothèque standard C ++ (par exemple, des ressources spécifiques à un système d'exploitation particulier).


    BTW, remarquez que le code converti (version agrégée) initialise la ressource en nullptr , et non à la mémoire allouée.


    "En code normal" pourrait être considéré comme un peu ... un-PC. Je pense que la définition du "code normal" est un doctorat en soi (l'informatique quantique prend un arc)


    5 Réponses :



    18
    votes

    1 commentaires

    Merci d'avoir clarifié cette gestion des ressources! = Pointer. Il ne m'est jamais venu à l'esprit que vous pouviez utiliser un INT pour la gestion des ressources ... maintenant je vois pourquoi RAII est nécessaire dans ce cas.



    7
    votes

    Avoir de bons concepts encapsulés qui suivent déjà la règle de cinq garantit en effet que vous devez moins en vous inquiéter. Cela dit, si vous vous trouvez dans une situation où vous devez écrire une logique personnalisée, elle se tient toujours. Certaines choses qui viennent à l'esprit:

    • Vos propres types de pointeurs intelligents
    • Observateurs qui doivent désinscrire
    • Emballages pour les bibliothèques C

    À côté de cela, je trouve qu'une fois que vous avez suffisamment de composition, il n'est plus clair quel sera le comportement d'une classe. Les opérateurs d'affectation sont-ils disponibles? Pouvons-nous copier la construction de la classe? Appliquant donc la règle de cinq, même avec = default , en combinaison avec - wdefauted-function-disseted car l'erreur aide à comprendre le code.

    pour regarder de plus près vos exemples:

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


    0 commentaires

    12
    votes

    La règle complète est, comme indiqué, la règle du 0/3/5; implémenter 0 d'entre eux généralement, et si vous implémentez un, implémentez 3 ou 5 d'entre eux.

    Vous devez implémenter les opérations de copie / déménagement et de destruction dans quelques cas.

  • Self référence. Parfois, les parties d'un objet se réfèrent à d'autres parties de l'objet. Lorsque vous les copiez, ils se référeront naïvement à l'objet autre que vous avez copié.

  • pointeurs intelligents. Il y a des raisons de mettre en œuvre plus de pointeurs intelligents.

  • Plus généralement que les pointeurs intelligents, les types de possession de ressources, comme vector ou facultatif ou variant s. Tous ces types de vocabulaire qui permettent à leurs utilisateurs de ne pas se soucier d'eux.

  • plus général que 1, objets dont l'identité est importante. Les objets qui ont un enregistrement externe, par exemple, doivent réengager la nouvelle copie avec le magasin de registres, et lorsqu'ils sont détruits, doivent se déranger.

  • Cas où vous devez être prudent ou sophistiqué en raison de la concurrence. À titre d'exemple, si vous avez un modèle mutex_guarded et que vous voulez qu'ils soient copyables, 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 devrez peut-être garantir l'ordre de certaines opérations, comparer et sets, ou même suivre ou enregistrer le "thread natif" de l'objet pour détecter quand il a franchi les limites du thread.


  • 0 commentaires

    5
    votes

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

    La version simplifiée va comme ceci: si vous devez écrire au moins une des (3/5) méthodes spéciales, vous devez écrire tous les (3/5).

    La règle réelle, utile : une classe responsable de la propriété manuelle d'une ressource devrait: traiter exclusivement la gestion de la propriété / durée de vie de la ressource; Pour ce faire correctement, il doit implémenter tous 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 de zéro).

    Les versions simplifiées utilisent cette rhétorique: si vous vous trouvez dans le besoin d'écrire l'une des (3/5), alors votre classe gère probablement manuellement la propriété d'une ressource, vous devez donc tout mettre en œuvre (3/5).

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

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

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

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

    Cependant, si vous vous trouvez dans le besoin de gérer manuellement une ressource, écrivez une classe RAII responsable exclusivement avec la gestion des ressources à vie. Cette classe doit implémenter tous les (3/5) membres spéciaux.

    Une bonne lecture à ce sujet: https://en.cprefreefre.com/w/ CPP / Language / Rule_OF_THREE


    0 commentaires