Quando ho imparato il C ++ molto tempo fa, mi è stato fortemente enfatizzato che parte del punto del C ++ è che, proprio come i loop hanno "loop-invarianti", anche le classi hanno invarianti associati alla vita dell'oggetto - cose che dovrebbero essere vere per tutto il tempo in cui l'oggetto è vivo. Cose che dovrebbero essere stabilite dai costruttori e preservate dai metodi. Il controllo di incapsulamento / accesso è lì per aiutarti a far rispettare gli invarianti. RAII è una cosa che puoi fare con questa idea.
Dal C ++ 11 ora abbiamo la semantica di spostamento. Per una classe che supporta lo spostamento, lo spostamento da un oggetto non termina formalmente la sua vita - la mossa dovrebbe lasciarlo in uno stato "valido".
Nel progettare una classe, è una cattiva pratica progettarla in modo tale che gli invarianti della classe siano preservati fino al punto da cui viene spostata? O va bene se ti permetterà di farlo andare più veloce.
Per renderlo concreto, supponiamo che io abbia un tipo di risorsa non copiabile ma mobile 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 motivo, ho bisogno di creare un wrapper copiabile per questo oggetto, in modo che possa essere utilizzato, forse in un sistema di spedizione 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 copyable_opaque
oggetto, un invariante della classe stabilita durante la costruzione è che il membro o_
punta sempre a un oggetto valido, poiché non esiste un ctor predefinito e l'unico ctor che non è un ctor copia garantisce questi. Tutti i operator()
metodi presuppongono che questo invariante valga e lo conservano in seguito.
Tuttavia, se l'oggetto viene spostato, quindi o_
non punta a nulla. E dopo quel punto, chiamare uno qualsiasi dei metodi operator()
causerà un arresto anomalo / UB.
Se l'oggetto non viene mai spostato, l'invariante verrà conservato fino alla chiamata del dtor.
Supponiamo che ipoteticamente, ho scritto questa lezione e mesi dopo, il mio collega immaginario ha sperimentato UB perché, in una complicata funzione in cui molti di questi oggetti venivano mescolati per qualche motivo, si è spostato da una di queste cose e in seguito ha chiamato uno di i suoi metodi. Chiaramente è colpa sua alla fine della giornata, ma questa classe è "mal progettata?"
Pensieri:
Di solito è una cattiva forma in C ++ creare oggetti zombi che esplodono se li tocchi.
Se non puoi costruire un oggetto, non puoi stabilire gli invarianti, quindi getta un'eccezione dal ctor. Se non riesci a preservare gli invarianti in qualche modo, allora segnala un errore in qualche modo e ripristina. Dovrebbe essere diverso per gli oggetti spostati?È sufficiente documentare "dopo che questo oggetto è stato spostato, è illegale (UB) farci qualcosa di 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 causano un rallentamento. Se il tuo progetto utilizza lo schema "build di rilascio / build di debug", piuttosto che solo sempre in esecuzione con asserzioni, immagino che questo sia più interessante, dal momento che non paghi per gli assegni nella build di rilascio. Se in realtà non hai build di debug, questo sembra abbastanza poco attraente.
- È meglio rendere la classe copiabile, ma non mobile?
Anche questo sembra negativo e provoca un calo delle prestazioni, ma risolve il problema "invariante" in modo semplice.
Quali considereresti le "migliori pratiche" pertinenti qui?