In generale, qualsiasi classe che gestisce una risorsa deve essere non copiabile o avere semantica della copia specializzata. Il contrario è vero: qualsiasi classe che non è copiabile o che ha bisogno di semantica della copia specializzata sta gestendo una risorsa. "Gestire una risorsa" nella lingua C++ significa in pratica un certo spazio in memoria, o una connessione a una rete o un database, un handle a un file, una transazione di annullamento e così via.
La gestione delle risorse cattura un bel po 'di esempi. Queste sono le responsabilità che richiedono un'operazione di prefisso, un'operazione di suffisso e, eventualmente, un'azione nel mezzo. La gestione della memoria, ad esempio, implica l'acquisizione di un handle per un indirizzo di memoria che gestiremo, magari con la memoria, e infine rilasciare l'handle (perché se ami qualcosa, lascia che sia libero).
template<typename T>
struct memory {
memory(T const& val = T()) : p(new T(val)) { }
~memory() { delete p }
T& operator*() const { return *p; }
private:
T* p;
};
// ...
{
memory<int> m0;
*m0 = 3;
std::cout << *m0 << '\n';
}
Questo memory
classe è quasi corretto: acquisisce automaticamente lo spazio di memoria sottostante e rilascia automaticamente, anche se un'eccezione si propaga qualche tempo dopo ha acquisito la sua risorsa. Ma considerare questo scenario:
{
memory<double> m1(3.14);
memory<double> m2(m1); // m2.p == m1.p (do you hear the bomb ticking?)
}
perché non abbiamo a disposizione la semantica di copia specializzati per memory
, il compilatore fornisce un proprio costruttore di copia e copiare l'assegnazione. Questi fanno la cosa errata: significa m2.p = m1.p
, tale che i due puntatori puntano allo stesso indirizzo.È sbagliato perché quando lo m2
esce dal campo di applicazione libera la sua risorsa come un buon oggetto responsabile, e quando lo m1
esce dallo scope libera anche la sua risorsa, la stessa risorsa m2
è già stata liberata, completando una doppia eliminazione - un famigerato scenario di comportamento non definito. Inoltre, in C++ è estremamente facile creare copie di un oggetto senza nemmeno accorgersene: una funzione prende il suo parametro in base al valore, restituisce il suo parametro in base al valore o prende il suo parametro per riferimento, ma chiama un'altra funzione che prende (o restituisce) stessa parametro per valore ... È più semplice presumere che le cose corrispondano a per essere copiati.
Tutto questo per dire che quando una raison d'être di classe sta gestendo una risorsa, immediatamente si deve sapere che è necessario gestire la copia. Si dovrebbe decidere
- sostenete la copia, mentre si decide cosa copiare significa: condivisione sicura delle risorse, l'esecuzione di una copia completa della risorsa sottostante quindi non c'è condivisione di sorta, o combinando i due approcci come in copy-on-write o copia pigra. Qualunque percorso tu scelga, dovrai fornire un costruttore di copie specializzato e un operatore di assegnazione copie.
- o non si supporta alcun tipo di copia della risorsa, nel qual caso si disabilita il costruttore di copie e l'operatore di assegnazione copia.
Mi piacerebbe andare così lontano e dire che la gestione delle risorse è l'unico caso in cui si disabilita la copia o fornire semantica della copia specializzata. Questa è solo un'altra prospettiva su The Rule of Three.
un singoletto sarebbe un esempio. –