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?