Copia di oggetti il costruttore di copia ha le stesse particolarità della signature di un costruttore ordinario; il primo parametro è una reference ad un oggetto della stessa classe. il compilatore provvede alla generazione automatica di un costruttore di copia, se non è presente nella classe. // point2d.h ... Point2D(const Point2D& other); // costruttore di copia nella sezione pubblica della classe // point2d.cpp Geometry::Point2D::Point2D(const Point2D& other) { x = other.X(); y = other.Y(); } un rvalue associato ad una const reference sopravvive oltre l’istruzione in cui è contenuto
è possibile usare il costruttore di copia non solo su lvalue, cioè entità che possono essere dereferenziate, ma anche sul risultato di espressioni #include "point2d.h" using Geometry::Point2D; int main() { Point2D p1(10, 10); // chiama il costruttore Point2D(double, double) Point2D p2(p1); // ok, chiama il costruttore di copia Point2D p3(Point2D(10, 10)); return 0; } a p3 è assegnato il risultato di un’espressione, cioè un valore temporaneo senza nome. Se il qualificatore const venisse omesso nel costruttore di copia della classe Point2D, la definizione di p3 produrrebbe un errore di compilazione Ok solo se il costruttore di copia accetta un valore temporaneo, cioè una const reference
Semantica del costruttore di copia il costruttore di copia per la classe Point2D è uguale a quello che il compilatore avrebbe generato per noi. la semantica del costruttore di copia generato automaticamente è quella della copia membro a membro. ma se una classe contiene dati membro che sono puntatori o reference, la copia membro a membro di un oggetto può non essere sufficiente; in questo caso la copia membro a membro è una copia “superficiale” (shallow copy). un’istanza della classe ed una sua shallow copy sono legate; una modifica fatta ad un dato membro puntatore o reference di una si riflette automaticamente nell’altra, con conseguenze che possono portare anche all’instabilità del programma. è importante implementare il costruttore di copia correttamente, eliminando le dipendenze tra un oggetto e le sue copie.
// a.h class A { public: A(); ~A(); A(const A& other); private: int m; int *ptr; }; // a.cpp ... A::A(const A& other) { m = other.m; // shallow copy // ptr = other.ptr; // deep copy ptr = new int; if (other.ptr != nullptr) *ptr = *(other.ptr); } deep copy: è allocata la memoria per il dato membro ptr, e poi è copiato il valore puntato da other.ptr. Ok solo se il costruttore di copia accetta un valore temporaneo, cioè una const reference
L’operatore di assegnamento di copia è usato quando ad un oggetto viene riassegnato un valore in una fase successiva alla sua dichiarazione, come nell’esempio seguente: Point2D a(1, 1); // invoca il costruttore Point2D(double, double) Point2D b(2, 2); // come sopra a = b; // invoca l'operatore di assegnamento di copia // point2d.h ... Point2D& operator=(const Point2D& other); // operatore di assegnamento di copia definito nella sezione pubblica // point2d.cpp Geometry::Point2D& Geometry::Point2D::operator=(const Point2D& other) { x = other.x; y = other.y; return *this; } La firma dell’operatore di assegnamento è analoga a quella del costruttore di copia; la sua implementazione, a meno del return *this, dovrebbe essere uguale a quella del costruttore. Anche l’operatore di assegnamento è generato automaticamente dal compilatore, se la nostra classe non ne definisce uno.
Return Value Optimization e elisione di copia La copia di oggetti è alla base di molte funzionalità del linguaggio C++: assegnazione e costruzione di copie, passaggio per valore dei parametri copia del valore di ritorno di una funzione algoritmi generici su strutture dati, eccetera. L’uso eccessivo della copia è una delle critiche rivolte al C++: cosa succede quando dobbiamo iterare la stessa funzione, ad esempio in un ciclo che scandisce una lista, o un altra struttura dati, con migliaia o milioni di oggetti complessi? La copia di un oggetto richiede memoria e cicli del processore. La copia di oggetti è l’operazione più costosa di tutte le istruzioni eseguite da una funzione. Nella fase di l’ottimizzazione del codice, lo standard C++ prescrive due azioni che possono essere applicate ogni volta che viene invocato il costruttore di copia.
ll caso più comune è quello dell’ottimizzazione del valore di ritorno di una funzione o RVO (return value optimization). L’ambito di visibilità del valore di ritorno di una funzione è limitato al corpo della funzione stessa; quando assegniamo ad una variabile il valore di ritorno di una funzione, in realtà il valore dovrebbe essere copiato nel contesto della chiamata a funzione. RVO è un’ottimizzazione che consiste nell’alterare la firma della funzione di modo da includere un parametro addizionale passato per riferimento, da usare al posto del valore di ritorno.
// funzione per il calcolo del punto medio di un segmento Point2D middlePoint(Point2D a, Point2D b) { Point2D result; result.setX((a.X() + b.X()) / 2.0); result.setY((a.Y() + b.Y()) / 2.0); return result; } ... Point2D c = middlePoint(Point2D(1, 1), Point2D(1, 2)); // codice equivalente a seguito dell'ottimizzazione RVO operata dal compilatore void middlePoint(Point2D a, Point2D b, Point2D& result) Point2D c; middlePoint(Point2D(1, 1), Point2D(1, 2), c); RVO dipende dal modo in cui scriviamo il nostro codice. Se la funzione presenta più di un possibile valore di ritorno, ad esempio dentro una condizione if–else, il compilatore potrebbe non essere in grado di ottimizzare il codice.
La seconda forma di ottimizzazione è l’elisione della copia (copy elision). E’ introdotta dal compilatore ogni volta che c’è un valore temporaneo: perchè sprecare risorse nel costruire un valore temporaneo al solo scopo di copiarlo in una variabile dello stesso tipo e distruggerlo immediatamente dopo? Point2D p3(Point2D(10, 10)); // ok se il costruttore di copia accetta un valore temporaneo, una const reference La maggior parte dei compilatori per C++ ottimizzerà la copia del temporaneo Point2D(10, 10) in una invocazione diretta al costruttore della classe, esattamente come se avessimo scritto: Point2D p3(10, 10);