Lorsque j'ai appris le C ++ il y a longtemps, il m'a été fortement souligné qu'une partie de l'intérêt du C ++ est que, tout comme les boucles ont des "invariants de boucle", les classes ont également des invariants associés à la durée de vie de l'objet - des choses qui devraient être vraies. tant que l'objet est vivant. Choses qui devraient être établies par les constructeurs et préservées par les méthodes. L'encapsulation / contrôle d'accès est là pour vous aider à appliquer les invariants. RAII est une chose que vous pouvez faire avec cette idée.
Depuis C ++ 11, nous avons maintenant la sémantique des mouvements. Pour une classe qui prend en charge le déplacement, le déplacement d'un objet ne met pas officiellement fin à sa durée de vie - le déplacement est censé le laisser dans un état "valide".
Lors de la conception d'une classe, est-ce une mauvaise pratique si vous la concevez de manière à ce que les invariants de la classe ne soient conservés que jusqu'au moment où elle est déplacée? Ou est-ce que ça va si cela vous permet d'aller plus vite.
Pour le rendre concret, supposons que j'ai un type de ressource non copiable mais mobile comme ceci:
class opaque {
opaque(const opaque &) = delete;
public:
opaque(opaque &&);
...
void mysterious();
void mysterious(int);
void mysterious(std::vector<std::string>);
};
Et pour une raison quelconque, j'ai besoin de créer un wrapper copiable pour cet objet, afin qu'il puisse être utilisé, peut-être dans un système de répartition existant.
class copyable_opaque {
std::shared_ptr<opaque> o_;
copyable_opaque() = delete;
public:
explicit copyable_opaque(opaque _o)
: o_(std::make_shared<opaque>(std::move(_o)))
{}
void operator()() { o_->mysterious(); }
void operator()(int i) { o_->mysterious(i); }
void operator()(std::vector<std::string> v) { o_->mysterious(v); }
};
Dans cet copyable_opaque
objet, un invariant de la classe établie à la construction est que le membre o_
pointe toujours vers un objet valide, car il n'y a pas de ctor par défaut, et le seul ctor qui n'est pas une copie ctor le garantit. Toutes les operator()
méthodes supposent que cet invariant est valable et le conservent ensuite.
Cependant, si l'objet est déplacé de, alors o_
ne pointera alors vers rien. Et après ce point, appeler l'une des méthodes operator()
entraînera un crash UB /.
Si l'objet n'est jamais déplacé, l'invariant sera conservé jusqu'à l'appel de dtor.
Supposons que, par hypothèse, j'ai écrit ce cours, et des mois plus tard, mon collègue imaginaire a fait l'expérience de l'UB parce que, dans une fonction compliquée où beaucoup de ces objets étaient mélangés pour une raison quelconque, il est passé de l'une de ces choses et plus tard appelé l'un des ses méthodes. Il est clair que c'est sa faute à la fin de la journée, mais cette classe est-elle "mal conçue?"
Pensées:
C'est généralement une mauvaise forme en C ++ de créer des objets zombies qui explosent si vous les touchez.
Si vous ne pouvez pas construire un objet, ne pouvez pas établir les invariants, alors lancez une exception du ctor. Si vous ne pouvez pas conserver les invariants dans une méthode, signalez une erreur d'une manière ou d'une autre et annulez. Cela devrait-il être différent pour les objets déplacés?Est-il suffisant de simplement documenter "après que cet objet a été déplacé, il est illégal (UB) de faire autre chose que de le détruire" dans l'en-tête?
Est-il préférable d'affirmer continuellement qu'il est valide dans chaque appel de méthode?
Ainsi:
class copyable_opaque {
std::shared_ptr<opaque> o_;
copyable_opaque() = delete;
public:
explicit copyable_opaque(opaque _o)
: o_(std::make_shared<opaque>(std::move(_o)))
{}
void operator()() { assert(o_); o_->mysterious(); }
void operator()(int i) { assert(o_); o_->mysterious(i); }
void operator()(std::vector<std::string> v) { assert(o_); o_->mysterious(v); }
};
Les assertions n'améliorent pas sensiblement le comportement, et elles provoquent un ralentissement. Si votre projet utilise le schéma "release build / debug build", plutôt que de simplement s'exécuter avec des assertions, je suppose que c'est plus attrayant, car vous ne payez pas les vérifications dans la version release. Si vous n'avez pas vraiment de versions de débogage, cela semble cependant assez peu attrayant.
- Est-il préférable de rendre la classe copiable, mais pas mobile?
Cela semble également mauvais et cause un impact sur les performances, mais cela résout le problème "invariant" de manière simple.
Selon vous, quelles sont les "meilleures pratiques" pertinentes ici?
la source
Réponses:
Mais ce n'est pas ce que tu fais. Vous créez un "objet zombie" qui explosera si vous le touchez mal . Ce qui n'est finalement pas différent de toute autre condition préalable basée sur l'État.
Considérez la fonction suivante:
Cette fonction est-elle sûre? Non; l'utilisateur peut passer un vide
vector
. Donc, la fonction a une condition préalable de fait quev
contient au moins un élément. Si ce n'est pas le cas, vous obtenez UB lorsque vous appelezfunc
.Cette fonction n'est donc pas "sûre". Mais cela ne signifie pas qu'il est cassé. Il n'est cassé que si le code qui l'utilise viole la condition préalable. Peut
func
- être est une fonction statique utilisée comme aide dans l'implémentation d'autres fonctions. Localisé de cette manière, personne ne l'appellerait d'une manière qui viole ses conditions préalables.De nombreuses fonctions, qu'elles aient une portée d'espace de noms ou des membres de classe, auront des attentes sur l'état d'une valeur sur laquelle elles opèrent. Si ces conditions préalables ne sont pas remplies, les fonctions échoueront, généralement avec UB.
La bibliothèque standard C ++ définit une règle "valide mais non spécifiée". Cela signifie que, sauf indication contraire de la norme, chaque objet déplacé sera valide (c'est un objet légal de ce type), mais l' état spécifique de cet objet n'est pas spécifié. Combien d'éléments comporte un déplacé
vector
? Ça ne dit pas.Cela signifie que vous ne pouvez appeler aucune fonction ayant une condition préalable.
vector::operator[]
a la condition préalable d'vector
avoir au moins un élément. Puisque vous ne connaissez pas l'état de lavector
, vous ne pouvez pas l'appeler. Ce ne serait pas mieux que d'appelerfunc
sans d'abord vérifier que levector
n'est pas vide.Mais cela signifie également que les fonctions qui n'ont pas de conditions préalables sont bonnes. C'est du code C ++ 11 parfaitement légal:
vector::assign
n'a pas de conditions préalables. Cela fonctionnera avec tout validevector
objet , même celui qui a été déplacé.Vous ne créez donc pas un objet cassé. Vous créez un objet dont l'état est inconnu.
Lancer des exceptions à partir d'un constructeur de mouvement est généralement considéré comme ... grossier. Si vous déplacez un objet qui possède de la mémoire, vous transférez la propriété de cette mémoire. Et cela n'implique généralement rien qui puisse jeter.
Malheureusement, nous ne pouvons pas appliquer cela pour diverses raisons . Nous devons accepter que lancer-déplacer est une possibilité.
Il convient également de noter que vous n'avez pas à suivre le langage "valide mais non spécifié". C'est simplement la façon dont la bibliothèque standard C ++ dit que le mouvement pour les types standard fonctionne par défaut . Certains types de bibliothèques standard ont des garanties plus strictes. Par exemple,
unique_ptr
est très clair sur l'état d'uneunique_ptr
instance déplacée : il est égal ànullptr
.Vous pouvez donc choisir de fournir une garantie plus forte si vous le souhaitez.
N'oubliez pas: le mouvement est une optimisation des performances , qui est généralement effectuée sur des objets sur le point d'être détruits. Considérez ce code:
Cela se déplacera de
v
dans la valeur de retour (en supposant que le compilateur ne l'élide pas). Et il n'y a aucun moyen de faire référence unev
fois le déménagement terminé. Donc, tout travail que vous avez fait pour mettrev
dans un état utile n'a pas de sens.Dans la plupart des codes, la probabilité d'utiliser une instance d'objet déplacé est faible.
L'intérêt d'avoir des conditions préalables est de ne pas vérifier de telles choses.
operator[]
a une condition préalable pourvector
avoir un élément avec l'index donné. Vous obtenez UB si vous essayez d'accéder en dehors de la taille du fichiervector
.vector::at
n'a pas une telle condition préalable; il lève explicitement une exception si levector
n'a pas une telle valeur.Des conditions préalables existent pour des raisons de performances. Ils sont pour que vous n'ayez pas à vérifier des choses que l'appelant aurait pu vérifier par lui-même. Chaque appel à
v[0]
n'a pas à vérifier s'ilv
est vide; seule la première le fait.Non. En fait, une classe ne devrait jamais être "copiable mais non mobile". S'il peut être copié, il doit pouvoir être déplacé en appelant le constructeur de copie. Il s'agit du comportement standard de C ++ 11 si vous déclarez un constructeur de copie défini par l'utilisateur mais ne déclarez pas de constructeur de déplacement. Et c'est le comportement que vous devez adopter si vous ne voulez pas implémenter de sémantique de déplacement spécial.
La sémantique de mouvement existe pour résoudre un problème très spécifique: traiter des objets qui ont de grandes ressources où la copie serait prohibitive ou sans signification (par exemple: les poignées de fichier). Si votre objet ne se qualifie pas, la copie et le déplacement sont les mêmes pour vous.
la source