Esercizio - Shallow copy e deep copy (quarto appello 2021-22, teoria)
Domanda aperta 1 del quarto appello 2021-22 (12 settembre 2022) del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. Teoria: C++ - costruttori, inizializzazione, copia e spostamentouna buona interfaccia è completa e minimale, ha i costruttori giusti, supporta o proibisce la copia, usa tipi adeguati, dichiara const ciò che non modifica e libera le risorse nel distruttore; il costruttore di default (senza argomenti) esiste quando c'è un valore iniziale sensato, si ottiene con valori di default nei membri (in-class initializer) o argomenti di default, e explicit vieta la conversione implicita da un argomento; initializer_list
Testo. Spiegare la differenza concettuale tra shallow copy e deep copy. Mostrare poi tale differenza usando due esempi di codice che implementano una classe che fa uso di deep copy e un'altra che fa uso di shallow copy: per tali classi creare il solo costruttore di copia (assumendo che tutte le altre funzioni membro necessarie siano già implementate) e il corpo della classe.
La differenza concettuale
Copiare un oggetto che contiene un puntatore a dei dati ha due possibili significati:
| shallow copy (copia superficiale) | deep copy (copia profonda) | |
|---|---|---|
| che cosa si copia | il valore del puntatore (l'indirizzo) | i dati puntati, in un nuovo buffer |
| dopo la copia | due oggetti puntano allo stesso buffer | ogni oggetto ha il suo buffer |
| modifica attraverso la copia | visibile nell'originale | indipendente |
| semantica | pointer (o reference) semantics | value semantics |
| costo | proporzionale ai dati | |
| rischio | doppia delete (due distruttori sullo stesso buffer) e dangling pointer |
nessuno (maggior costo) |
| quando serve | tipi che non possiedono i dati (viste) o con proprietà condivisa gestita (contatori) | tipi che possiedono i dati: vector, string |
La copia di default (membro a membro) è una shallow copy per ogni puntatore: per questo una classe che possiede memoria dinamica deve definire costruttore e assegnamento di copia, oppure vietarli. Un tipo con una deep copy ha value semantics ed è un po' più lento.
Due classi
Entrambe hanno un int con la dimensione e un puntatore a un buffer. L'unica differenza è il costruttore di copia. La classe shallow non libera nulla nel distruttore: con una shallow copy due oggetti avrebbero lo stesso buffer e lo libererebbero due volte.
#include <iostream>
using namespace std;
// ---------- classe con SHALLOW copy: il costruttore di copia copia solo il puntatore ----------
class ShallowArray {
public:
explicit ShallowArray(int n) : n_{n}, a_{new int[n]()} {}
ShallowArray(const ShallowArray& o) // costruttore di copia SHALLOW
: n_{o.n_}, a_{o.a_} // stesso indirizzo: i due oggetti condividono il buffer
{}
// niente distruttore che liberi: con una shallow copy due oggetti libererebbero lo stesso buffer
int& operator[](int i) { return a_[i]; }
int* data() const { return a_; }
void libera() { delete[] a_; a_ = nullptr; } // da chiamare UNA volta, a mano, da un solo proprietario
private:
int n_;
int* a_;
};
// ---------- classe con DEEP copy: il costruttore di copia duplica i dati ----------
class DeepArray {
public:
explicit DeepArray(int n) : n_{n}, a_{new int[n]()} {}
DeepArray(const DeepArray& o) // costruttore di copia DEEP
: n_{o.n_}, a_{new int[o.n_]} // NUOVO buffer
{
for (int i = 0; i < n_; ++i) a_[i] = o.a_[i]; // si copiano i valori
}
~DeepArray() { delete[] a_; } // qui ogni oggetto ha il suo buffer: la delete e' corretta
int& operator[](int i) { return a_[i]; }
int* data() const { return a_; }
private:
int n_;
int* a_;
};
int main()
{
ShallowArray s1(3);
s1[0] = 1;
ShallowArray s2 = s1; // shallow
s2[0] = 99;
cout << "shallow: stesso buffer? " << (s1.data() == s2.data()) << ", s1[0] = " << s1[0] << " (modificato tramite s2)\n";
s1.libera(); // libero il buffer una sola volta; s2 ora e' pendente: non si usa piu'
DeepArray d1(3);
d1[0] = 1;
DeepArray d2 = d1; // deep
d2[0] = 99;
cout << "deep: stesso buffer? " << (d1.data() == d2.data()) << ", d1[0] = " << d1[0] << ", d2[0] = " << d2[0] << '\n';
return 0;
}shallow: stesso buffer? 1, s1[0] = 99 (modificato tramite s2)
deep: stesso buffer? 0, d1[0] = 1, d2[0] = 99ShallowArray: ShallowArray(const ShallowArray& o) : n_{o.n_}, a_{o.a_} {} copia l'indirizzo: dopo ShallowArray s2 = s1;, s1.data() == s2.data() è vero e s2[0] = 99 cambia anche s1[0]. È una classe che per essere corretta richiede un proprietario unico che chiami libera() una volta (qui a mano), oppure un conteggio dei proprietari (shared_ptr, C++ - smart pointerGli smart pointer (classi template di ~ShallowArray() { delete[] a_; }, la fine di main farebbe delete[] due volte sullo stesso buffer: comportamento indefinito.
DeepArray: DeepArray(const DeepArray& o) : n_{o.n_}, a_{new int[o.n_]} alloca un nuovo buffer, poi il ciclo copia i valori: d2[0] = 99 non tocca d1[0] (i buffer sono distinti, d1.data() != d2.data()); ogni oggetto libera il proprio, quindi il distruttore delete[] a_ è corretto. Con la deep copy anche l'assegnamento di copia deve essere definito (alloca, copia, libera il vecchio): altrimenti d2 = d1; farebbe di nuovo una shallow copy.
Versione ripasso
- Shallow copy = copia dell'indirizzo (due oggetti, stesso buffer: modifiche visibili in entrambi, pericolo di doppia
delete); deep copy = copia dei dati in un nuovo buffer (indipendenti, più lenta). Pointer semantics contro value semantics. La copia di default è shallow per i puntatori con memoria dinamica si definiscono costruttore e assegnamento di copia (o si vietano).
class ShallowArray { // copia l'indirizzo
public: ShallowArray(const ShallowArray& o) : n_{o.n_}, a_{o.a_} {} /* niente delete[] nel distruttore */
private: int n_; int* a_; };
class DeepArray { // copia i dati
public: DeepArray(const DeepArray& o) : n_{o.n_}, a_{new int[o.n_]}
{ for (int i = 0; i < n_; ++i) a_[i] = o.a_[i]; }
~DeepArray() { delete[] a_; }
private: int n_; int* a_; };- Prova: shallow:
s1.data() == s2.data()vero,s2[0] = 99s1[0] = 99; deep: buffer diversi,d1[0] = 1,d2[0] = 99. - Errori tipici: distruttore con
delete[]in una classe con copia shallow (doppiadelete); deep copy senza assegnamento di copia; copiaren_ma non allocare il buffer; credere che la copia di default copi anche i dati puntati.