Semantica di Copia: Profonda vs Superficiale
Impara la differenza tra copie superficiali e profonde, come implementare operazioni di copia corrette per le classi che possiedono risorse, e l'idioma copy-and-swap.
Perché la Semantica di Copia è Importante
Quando copi un oggetto in C++, che cosa viene copiato esattamente? Per i tipi semplici la risposta è immediata — tutti i membri vengono copiati bit per bit. Ma quando un oggetto possiede una risorsa attraverso un puntatore, il costruttore di copia predefinito (generato dal compilatore) esegue una copia superficiale: copia il valore del puntatore, non i dati a cui punta. Questo significa che due oggetti condividono ora la stessa risorsa, con conseguenti bug di doppia deallocazione e corruzione dei dati.
Capire la semantica di copia è essenziale, perché le copie avvengono implicitamente in molti punti: passaggio per valore, ritorno da funzioni, inserimento nei contenitori e assegnamento.
Il Problema della Copia Superficiale
Il costruttore di copia generato dal compilatore copia ogni membro. Per i puntatori, copia l'indirizzo — non i dati puntati. Questa è una copia superficiale ed è pericolosa per le classi che possiedono risorse.
#include <iostream>
#include <cstring>
class BrokenString {
char* data_;
std::size_t len_;
public:
BrokenString(const char* s)
: len_{std::strlen(s)}
, data_{new char[std::strlen(s) + 1]}
{
std::strcpy(data_, s);
}
~BrokenString() {
delete[] data_; // frees the buffer
}
// No copy constructor defined!
// Compiler generates shallow copy: copies the pointer
void print() const {
std::cout << data_ << '\n';
}
};
int main() {
BrokenString a{"Hello"};
{
BrokenString b = a; // shallow copy: b.data_ == a.data_
b.print(); // "Hello" — works for now
} // b destroyed: delete[] b.data_ (same pointer as a.data_!)
// a.data_ is now a dangling pointer — UNDEFINED BEHAVIOR
a.print(); // may crash, print garbage, or appear to work
return 0;
}Implementare la Copia Profonda
Una copia profonda alloca nuova memoria e copia i dati veri e propri. Questo garantisce che ogni oggetto possieda la propria copia indipendente della risorsa.
#include <iostream>
#include <cstring>
#include <algorithm>
class SafeString {
char* data_;
std::size_t len_;
public:
explicit SafeString(const char* s)
: len_{std::strlen(s)}
, data_{new char[std::strlen(s) + 1]}
{
std::strcpy(data_, s);
}
// Deep copy constructor
SafeString(const SafeString& other)
: len_{other.len_}
, data_{new char[other.len_ + 1]}
{
std::strcpy(data_, other.data_);
}
// Deep copy assignment with self-assignment check
SafeString& operator=(const SafeString& other) {
if (this == &other) return *this; // self-assignment guard
// Allocate new resource BEFORE releasing old one
char* new_data = new char[other.len_ + 1];
std::strcpy(new_data, other.data_);
// Release old resource
delete[] data_;
// Adopt new resource
data_ = new_data;
len_ = other.len_;
return *this;
}
~SafeString() { delete[] data_; }
void print() const {
std::cout << data_ << " (len=" << len_ << ")\n";
}
};
int main() {
SafeString a{"Hello"};
SafeString b = a; // deep copy: b gets its own buffer
SafeString c{"World"};
c = a; // deep copy assignment
a.print(); // Hello (len=5)
b.print(); // Hello (len=5)
c.print(); // Hello (len=5)
return 0;
}L'Idioma Copy-and-Swap
L'idioma copy-and-swap offre un'implementazione elegante e sicura rispetto alle eccezioni dell'operatore di assegnamento per copia. Riutilizza il costruttore di copia e una funzione swap, fornendo la garanzia forte rispetto alle eccezioni con una duplicazione di codice minima.
#include <iostream>
#include <cstring>
#include <utility>
class Buffer {
char* data_;
std::size_t size_;
public:
explicit Buffer(std::size_t size)
: size_{size}, data_{new char[size]{}} {}
Buffer(const Buffer& other)
: size_{other.size_}, data_{new char[other.size_]}
{
std::memcpy(data_, other.data_, size_);
}
~Buffer() { delete[] data_; }
// Copy-and-swap: take parameter by VALUE (invokes copy ctor)
Buffer& operator=(Buffer other) { // copy happens here
swap(*this, other); // swap resources
return *this;
} // 'other' destroyed here, freeing our old resource
// Non-throwing swap — essential for strong exception safety
friend void swap(Buffer& a, Buffer& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
swap(a.size_, b.size_);
}
char& operator[](std::size_t i) { return data_[i]; }
std::size_t size() const { return size_; }
};
int main() {
Buffer a{5};
a[0] = 'H'; a[1] = 'i';
Buffer b{10};
b = a; // copy-and-swap: exception-safe, self-assignment-safe
std::cout << b[0] << b[1] << " (size " << b.size() << ")\n";
return 0;
}Disabilitare la Copia
Alcuni tipi non dovrebbero essere copiabili — per esempio i tipi che rappresentano risorse uniche come handle di file, connessioni di rete o thread. Usa =delete per comunicarlo chiaramente e farlo rispettare:
class UniqueConnection {
public:
UniqueConnection(const UniqueConnection&) = delete;
UniqueConnection& operator=(const UniqueConnection&) = delete;
// Le operazioni di spostamento possono comunque essere abilitate
};Prima del C++11 questo si faceva dichiarando le operazioni di copia private senza definirle — un espediente poco chiaro che =delete sostituisce con eleganza.
Un assegnamento per copia ingenuo che cancella la vecchia risorsa prima di allocare la nuova è sbagliato in due modi: (1) l'auto-assegnamento (a = a) cancella i dati e poi prova a copiarli da se stessi, e (2) se new lancia un'eccezione dopo il delete, l'oggetto resta in uno stato non valido. L'idioma copy-and-swap risolve entrambi i problemi: la copia viene fatta prima di qualsiasi modifica, e lo swap è noexcept.
Il modo migliore per evitare bug di copia è non gestire mai direttamente risorse grezze. Usa std::string al posto di char*, std::vector al posto degli array grezzi e std::unique_ptr al posto dei puntatori grezzi proprietari. Quando tutti i tuoi membri hanno una semantica di copia corretta, le operazioni di copia generate dal compilatore sono automaticamente corrette. Questa è la Regola dello Zero messa in pratica.
- La copia generata dal compilatore è superficiale — copia i valori dei puntatori, non i dati puntati
- Le classi che possiedono risorse hanno bisogno di un costruttore di copia e di un operatore di assegnamento con copia profonda
- L'idioma copy-and-swap ti regala sicurezza rispetto alle eccezioni e sicurezza nell'auto-assegnamento
- Usa
=deleteper vietare esplicitamente la copia dei tipi che rappresentano risorse uniche - La strategia migliore è la Regola dello Zero: usa wrapper RAII così da non dover mai scrivere logica di copia personalizzata
Quiz — Metti alla prova le tue conoscenze
(15 XP)1. Cosa succede quando copi in modo superficiale un oggetto che possiede memoria heap tramite un puntatore grezzo?
2. Perché l'idioma copy-and-swap prende il parametro per valore?
3. Quale approccio è consigliato per non aver bisogno affatto di operazioni di copia personalizzate?