Copy Semantics: Costruttori e Assegnazioni di Copia
La Copy Semantics definisce il significato dell’atto di copiare in C++. Generalmente vogliamo che la regola d’oro sia: dopo che x è stato copiato in y, i due oggetti sono equivalenti ma indipendenti (una modifica a y non influirà più su x).
Per i tipi elementari (come interi e float) e per le normali struct (POD - Plain Old Data), il compilatore attua tacitamente una copia memberwise (membro a membro). Ovvero, prende il contenuto byte-per-byte della prima e lo trascrive nella seconda. Tutto bene, fintanto che la nostra classe non gestisce risorse allocate dinamicamente!
Il Pericolo della Copia Memberwise (Shallow Copy)
Section titled “Il Pericolo della Copia Memberwise (Shallow Copy)”Pensa a un oggetto Stringa che possiede un puntatore a un buffer dinamicamente allocato con new[].
Se lo passiamo per valore a una funzione o lo cloniamo in un altro oggetto, il compilatore copierà anche lui membro a membro… incluso il puntatore!
Adesso possediamo due oggetti diversi che puntano allo stesso identico pezzo di memoria. Se l’oggetto A cambia la stringa, anche B ne subisce gli effetti (perdita di indipendenza).
E il peggio deve arrivare: alla fine del blocco di esecuzione, quando interverranno i Distruttori di entrambi gli oggetti, entrambi chiameranno delete[] sullo stesso buffer.
Questo si chiama Double Free. Risultato? Corruzione totale della memoria e crash inesorabile.
Il Copy Constructor (Costruttore di Copia)
Section titled “Il Copy Constructor (Costruttore di Copia)”Per evitare questo dramma, implementiamo noi stessi una “Copia Profonda” (Deep Copy), prendendo in mano il Costruttore di Copia. Un costruttore di copia prende in input una const lvalue reference allo stesso tipo di oggetto.
struct SimpleString { size_t max_size; char* buffer;
// Costruttore normale SimpleString(size_t max_size) : max_size{max_size}, buffer{new char[max_size]} {}
// Distruttore che garantisce il rilascio! ~SimpleString() { delete[] buffer; }
// COSTRUTTORE DI COPIA SimpleString(const SimpleString& other) : max_size{other.max_size}, buffer{new char[other.max_size]} // Allocazione *indipendente* { // Copio i dati veri e propri usando std::strncpy o cicli }};Quando creiamo un oggetto da uno vecchio usando la sintassi di inizializzazione (SimpleString b{a};) interverrà questo costruttore, scongiurando il double-free e duplicando fisicamente la memoria!
Copy Assignment (Assegnazione di Copia)
Section titled “Copy Assignment (Assegnazione di Copia)”L’altro momento in cui la copia entra in azione è l’assegnazione usando =.
La differenza rispetto al costruttore di copia è che in un’assegnazione (b = a;), l’oggetto b esiste già, ha già della memoria allocata, che potrebbe essere più grande o più piccola di quella che serve adesso.
Nell’Assegnazione di Copia (definita tramite operator=), dobbiamo:
- Liberare le risorse attuali dell’oggetto ricevente.
- Allocare quelle nuove.
- Copiare il dato dal lato destro.