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?
5 Réponses :
Le nom complet de la règle est La règle du 3/5/0 a >.
Il ne dit pas "Fournissez toujours les cinq". Il dit que vous devez fournir les trois, les cinq ou aucun d'entre eux.
En effet, le plus souvent le mouvement le plus intelligent est de ne fournir aucun des cinq. Mais vous ne pouvez pas le faire si vous écrivez votre propre conteneur, votre pointeur intelligent ou un wrapper Raii autour de certaines ressources.
Événement Cette version de la règle n'est pas celle qui devrait toujours être suivi. Il y a des exceptions.
@eerorika curieux, quelles sont les exceptions? Je ne pense pas en avoir vu.
Disons que vous devez avoir un pointeur qui pointe vers un membre. Si vous copiez l'objet, vous devez mettre à jour ce pointeur. Ainsi, vous avez besoin d'un constructeur de copie personnalisé (ou supprimé) et d'un opérateur d'affectation. Vous n'avez pas besoin d'un destructeur.
@HolyBlackCat, j'ai une classe qui est un wrapper C ++ autour d'une connexion de base de données SQLite. Il a un destructeur (donc la connexion est fermée lorsque l'objet est détruit), mais le fonctionnement correct nécessite que l'objet de connexion soit unique: c'est une erreur pour appeler un constructeur de copie, un opérateur d'affectation ou toute autre chose qui créerait un deuxième emballage d'objet la même connexion.
@Mark cela nécessite = supprimer
des opérations de copie, ce que l'OMI compte comme les fournissant aux fins de la règle de 3.
Cependant, dans le 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 peu d'invariant, donc orthogonal avec la règle de 5.
Comme par exemple, un
struct clampInt { int min; int max; int value; };
ne garantit pas que min
Quand avez-vous déjà besoin d'un destructeur défini par l'utilisateur, d'un constructeur de copie, d'un constructeur d'attribution de copie, d'un constructeur de déplacements ou d'un constructeur d'attribution de déplacement?
Maintenant, sur la règle du 5/3/0.
En effet, la règle de 0 doit être préférée.
disponibles disponibles (j'inclus le conteneur) sont pour pointeurs, collections ou Lockables .
Mais les ressources ne sont pas les pointeurs nécessaires (pourrait être manche caché dans un int
, variables statiques cachées internes ( xxx_init ()
/ xxx_close ( . Vous pourriez également vouloir écrire un objet RAII qui ne possède pas vraiment de ressource, en tant que
timerlogger
par exemple (écrivez le temps écoulé utilisé par une "portée").
Un autre moment où Vous devez généralement écrire Destructor est pour une classe abstraite, car vous avez besoin de destructeur virtuel (et une copie polymorphe possible se fait par un virtuel> clone
).
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.
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:
À 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); };
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.
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). p >
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
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
ennullptr
, 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)