Quando ho imparato il C ++ molto tempo fa, mi è stato strongmente sottolineato che parte del punto di C ++ è che proprio come i loop hanno "invarianti di loop", le classi hanno anche invarianti associati alla durata dell'oggetto - cose che dovrebbe essere vero per tutto il tempo in cui l'oggetto è vivo. Cose che dovrebbero essere stabilite dai costruttori e preservate dai metodi. L'incapsulamento / controllo degli accessi è lì per aiutarti a rafforzare gli invarianti. RAII è una cosa che puoi fare con questa idea.
Da C ++ 11 ora abbiamo la semantica del movimento. Per una classe che supporta lo spostamento, lo spostamento da un oggetto non termina formalmente la sua vita - la mossa dovrebbe lasciarla in uno stato "valido".
Nel progettare una classe, è cattiva pratica se la si progetta in modo tale che gli invarianti della classe vengano mantenuti solo fino al punto da cui viene spostato? O è ok se ti permetterà di farlo andare più veloce.
Per renderlo concreto, supponiamo di avere un tipo di risorsa non copiabile ma spostabile in questo modo:
class opaque {
opaque(const opaque &) = delete;
public:
opaque(opaque &&);
...
void mysterious();
void mysterious(int);
void mysterious(std::vector<std::string>);
};
E per qualsiasi ragione, ho bisogno di creare un wrapper copiabili per questo oggetto, in modo che possa essere usato, forse in un sistema di invio esistente.
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); }
};
In questo oggetto copyable_opaque
, un invariante della classe stabilita in costruzione è che il membro o_
punti sempre a un oggetto valido, poiché non esiste un ctor predefinito e l'unico ctor che non è un ctor di copia garantisce questi. Tutti i metodi operator()
presumono che questo invariante regga e lo conservino in seguito.
Tuttavia, se l'oggetto viene spostato da, quindi o_
non punta a nulla. E dopo quel punto, chiamare uno qualsiasi dei metodi operator()
causerà UB / un arresto anomalo.
Se l'oggetto non viene mai spostato da, l'invariante verrà mantenuto fino alla chiamata dtor.
Supponiamo che ipoteticamente, ho scritto questa lezione, e mesi dopo, il mio collega immaginario ha fatto esperienza di UB perché, in alcune complicate funzioni in cui molti di questi oggetti venivano mescolati per qualche motivo, si è trasferito da una di queste cose e in seguito ha chiamato uno dei suoi metodi. Chiaramente è colpa sua alla fine della giornata, ma questa classe è "mal progettata?"
Pensieri:
-
Di solito è una cattiva forma in C ++ per creare oggetti zombi che esplodono se li tocchi.
Se non puoi costruire qualche oggetto, non puoi stabilire gli invarianti, quindi lanciare un'eccezione dal ctor. Se non è possibile conservare gli invarianti in qualche metodo, segnalare un errore in qualche modo e il rollback. Dovrebbe essere diverso per gli oggetti spostati da? -
È sufficiente documentare semplicemente "dopo che questo oggetto è stato spostato da, è illegale (UB) fare qualsiasi cosa con esso diverso da distruggerlo" nell'intestazione?
-
È meglio affermare continuamente che è valido in ogni chiamata di metodo?
Così:
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); }
};
Le asserzioni non migliorano sostanzialmente il comportamento e provocano un rallentamento. Se il tuo progetto usa lo schema "build build / debug build", invece di essere sempre in esecuzione con asserzioni, immagino che questo sia più allettante, dal momento che non paghi i controlli nella build di rilascio. Se in realtà non hai build di debug, questo sembra piuttosto poco attraente però.
- È meglio rendere la classe copiabile, ma non mobile?
Anche questo sembra male e causa un colpo di prestazioni, ma risolve il problema "invariante" in modo semplice.
Quali considereresti essere le "migliori pratiche" pertinenti qui?