giovedì 31 marzo 2011

Shallow Copy e Deep Copy in C++

In questo articolo cercheremo di capire cosa si intende per shallow copy e deep copy di un oggetto: tali concetti saranno validi per tutti i linguaggi di programmazione ad oggetti, ma nel linguaggio C++, che fa uso di puntatori e della copia implicita di oggetti,  rivestono un’importanza maggiore. Iniziamo a definire cosa è un costruttore di copia in C++: un costruttore di copia è un costruttore speciale, utilizzato per creare un nuovo oggetto-copia di un oggetto esistente. Ma perchè ne abbiamo bisogno? Se noi non definiamo un costruttore di copia sarà il compilatore a crearne uno per noi, ossia creerà un costruttore di copia di default. Con tale costruttore di copia, la copia avverrà “bit a bit”, brutto modo per dire che che si avrà una copia superficiale dell’ oggetto (shallow copy). Se noi vogliamo ottenere una copia dell’oggetto differente e in profondità (deep copy), dobbiamo crearcene uno.
Supponiamo inizialmente di avere la seguente classe C++:

File A.h
class A
{
public:
       A(int i) { a = i; }
       ~A(void) { }
       int get_a() { return a; }

private :
       int a;

};

Supponiamo che adesso si effettui una copia di un oggetto in un altro oggetto in un programma che usa tale classe:

#include "A.h"
#include <iostream>
using namespace std;

     
A x(10);
A y(20);
x = y; //shallow copy
cout << x.get_a()<<" "<< y.get_a() << endl;

Tutto è corretto: l’oggetto “y” viene copiato in “x” bit a bit, ossia il valore dell' attributo intero di y di valore 20 viene copiato in quello di x, cosicchè il programma stamperà 20 20.
Ma cosa succederebbe se al posto di un intero avessimo un puntatore? Si consideri ora la stessa classe A, dove abbiamo sostituito l'attributo intero con un puntatore ad un intero:

class A
{
public:
       A(int i)
{
                    a = new int;
*a = i;
}
       ~A(void) { delete a;}
       int get_a() { return *a; }

private :
       int* a;
};

Se provo a lanciare il programma esso stampa ancora 20 20, quindi sembra che tutto sia andato bene, ma non è così! Ciò che succede quando eseguo x = y, è che viene copiato il puntatore di y in quello di x:



Questo, oltre ad essere sbagliato concettualmente (non si effettua una reale copia dell’oggetto), dà vita anche ad un bel memory leak!
Si noti che nell’esempio con l’istruzione x = y, abbiamo chiamato l’operatore “=” effettuando una shallow copy, ma se per esempio avessimo usato una funzione f, che accetta in ingresso un oggetto A passato per valore, il discorso non cambierebbe perché in quel caso verrebbe creato automaticamente un oggetto A locale su cui verrà copiato l’oggetto passato:

A x(10);
f(x); //questa funzione "distrugge" l'intero puntato dal membro di x
cout << x.get_a()<< endl; 

...

void f(A y)
{
//qui y è una shallow copy di x
}
        
In questo caso verrà chiamato automaticamente il costruttore di copia di default della classe A (definito dal compilatore), che effettua anch’esso una copia bit a bit. Se proviamo a stampare il valore dell'intero puntato dal membro di x dopo la chiamata a f(x), vedremo che tale valore è stato "cancellato" dalla delete dentro il distruttore chiamato sull'oggetto locale y all'uscita dalla funzione.
Il problema si risolve dichiarando esplicitamente il costruttore di copia e l’operatore di uguaglianza e copiando “manualmente” i valori puntati dai membri puntatore. Nel nostro caso ci servirà definire dentro la classe A i due metodi:

A(const A& val); //costruttore di copia
A& operator=(const A& val); //sovraccaricamento operatore =

La classe A diventerà:

class A
{
public:
       A(int i)
{
                    a = new int;
*a = i;
}
       ~A(void) { delete a;}
       int get_a() { return *a;}

A(const A& val) //costruttore di copia
{
                     a = new int;
                    *a = *(val.a);
}
A& operator=(const A& val) //sovraccaricamento operatore =
{
                    *a = *(val.a);
                    return *this;
}

private :
       int* a;
};

Come si nota, sia il costruttore di copia che l’operatore “=” accettano entrambi una reference ad un altro oggetto A e copiano manualmente il valore intero, in modo da avere una copia effettiva:


Bisogna fare molta attenzione al distruttore della classe A: esso giustamente distrugge il puntatore “a” quando l’oggetto A viene distrutto: se si omette la delete dentro il distruttore, potremmo avere “piacevoli” sorprese: a volte si sa che in informatica c’è la regola “bug + bug = funziona tutto”! Facciamo un esempio, supponendo che nella classe A senza costruttore di copia e operatore =, manchi pure la delete e che vogliamo effettuare lo swap di due oggetti (ossia del contenuto dell’intero puntato da “a”):

A x(10);
A y(20);

cout << "x=" << x.get_a()<<" y="<<y.get_a() << endl;
A::swap(x,y);
cout << "x=" << x.get_a()<<" y="<<y.get_a() << endl;

dove abbiamo definito in A il metodo statico:

static void swap(A& x, A& y)//passaggio per reference, nessuna copia!
{
       A tmp = x; //questo chiama il costruttore di copia bit a bit
       x = y; //questo chiama l’operatore = bit a bit
       y = tmp; /questo chiama l’operatore = bit a bit
}

Con somma meraviglia notiamo che effettivamente la funzione swap effettua lo scambio delle variabili intere nonostante la shallow copy e il programma sopra stampa:

x=10 y=20
x=20 y=10

Ma vediamo graficamente cosa avviene. Dopo l’istruzione A tmp = x si avrà:


Naturalmente il fatto che swap funzioni è solo un caso e resta il fatto che le variabili intere puntate, in questo modo non verrebbero mai eliminate. Provando ad aggiungere nuovamente la delete al distruttore di A (ma senza aggiungere anche il costruttore di copia e l’operatore =), otterremmo dal programma precedente:

x=10 y=20
x=20 y= -17891602

Questo perchè all’uscita della funzione swap, l’intero puntato da tmp (oggetto locale alla funzione), viene cancellato dalla delete (in rosso in figura), lasciando “flottante” anche il puntatore contenuto dentro y, tale comportamento può considersi della categoria "effetti collaterali". Questo esempio è stato fatto solo per far capire che con i puntatori a volte anche se le cose sembrano “funzionare”, in realtà non è affatto così.
La regola che si deve tenere dovrebbe essere: “se una classe non alloca nei propri membri memoria in maniera dinamica (new, malloc ecc.), allora possiamo affidarci alla shallow copy, altrimenti è necessaria la deep copy”

In C++ la deep copy consiste in:
  • Creazione di un costruttore di copia che allochi la memoria e copi i valori delle variabili dell’oggetto da copiare.
  • Sovraccaricamento dell’operatore = che copi i valori delle variabili da un oggetto all’altro
  • Creazione di un distruttore che deallochi tutte le variabili allocate dinamicamente

Per approfondimenti sulla Shallow Copy e Deep Copy, anche in altri linguaggi, si legga qui.

0 commenti:

Posta un commento

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | cna certification