Newsgroup: it.comp.lang.c++
Titolo: Costruttore di copia

Sto leggendo Inside the c++ object model di Lippman, ed in particolare
ho appena letto la parte relativa al costruttore di copia, però (causa
il mio pessimo inglese provabilmente) non mi è ancora ben chiaro in
quali casi sia necessario utilizzare un costruttore di copia.
Qualcuno può chiarirmi l'argomento?
grazie
Ciao
Terenzio


Fondamentalmente, quando la classe contiene elementi allocati
dinamicamente e quindi il costruttore di copia generato automaticamente
dal compilatore non sarebbe sufficiente.
EMG

 wrote:
Di solito quando la classe A implementa
il contenimento tramite puntatori. In questi casi
anche per le risorse puntate viene creata una copia e
i puntatori della copia dell'oggetto di tipo A devono puntare
alle copie delle risorse, non alle risorse originali.
Paolo
----------------------------------------------------------
  Paolo Marotta
  Computer Science, University of Pisa
  home:   
http://www.cli.di.unipi.it/~marotta
  e-mail: 
mar...@cli.di.unipi.it
 
----------------------------------------------------------

Qualcuno può chiarirmi l'argomento?
Il costruttore di copia di una classe X viene invocato automaticamente
dal compilatore tutte le volte che:
    1. passi un argomento di classe X per valore,
    2. ritorni un valore di classe X da una funzione.
Inoltre, lo invochi esplicitamente tu quando dichiari una nuova
variabile di classe X, inizializzandola con il valore di una gia`
disponibile (operazione che fa comodo abbastanza spesso, per
fare operazioni che possono causare modifiche; modifichi la
copia in modo che l'originale resti intonso).
Alex

dal compilatore non sarebbe sufficiente.
Questi sono i casi in cui e` necessario *definirlo*, non quelli
in cui e` necessario *utilizzarlo* (che sono: passaggio di
parametri per valore, ritorno per valore da una funzione, uso
esplicito per generare un nuovo oggetto che inizia come copia
di uno gia` esistente).
Alex


Lo so, solo che nella sfretta avevo capito che il riferimento fosse al caso
in cui era necessario definirlo (anche perche` negli altri casi, a meno di indicazioni
esplicite del programmatore, e` il compilatore stesso a generarne uno).
Ad ogni modo avevo letto la tua risposta al quesito iniziale e non mi era sembrato
necessario aggiungere altro (chi tace acconsente... no?).


 di uno gia` esistente).
Si, ma immaginavo che l'amico, anche se in termini non precisissimi,
chiedesse lumi sulla definizione del copy ctor, che č anche la parte piů
"attiva", e non sull'uso magari implicito dello stesso.
EMG

"attiva", e non sull'uso magari implicito dello stesso.
Puo` benissimo darsi che tu abbia ragione.  In questo caso,
merita forse accennare che, in C++ Standard usato al meglio,
i costruttori di copia andrebbero *definiti* abbastanza raramente,
e di solito per classi ausiliarie di basso livello; una classe "di
rilevanza alla logica applicativa", *normalmente*, dovrebbe
essere definita in modo tale che il costruttore di copia di
default (che costruisce applicando i costruttori di copia a
ciascuna base e membro) risulti sufficiente.
[Avvertimento: alcuni potrebbero ritenere "avanzate" parti
del testo e degli esempi seguenti; richiedono una discreta
conoscenza del C++ standard {e un compilatore non mille
miglia lontano dallo standard stesso}, e hanno lo scopo di
mostrare uno stile di utilizzo di alcuni strumenti messi a
disposizione dallo standard per migliorare la qualita` del
proprio codice].
Per riassumere, possiamo dire che la ragione e` legata
anzitutto alla "exception safety".  Se si verifica una
eccezione nel corpo di un costruttore per la classe X
(compreso un costruttore di copia), non viene eseguito
il distruttore di X (vengono, pero`, correttamente eseguiti
i distruttori dei _membri_ di X, e delle sue _basi_, in
quanto, se abbiamo iniziato l'esecuzione del corpo del
costruttore, tutti i membri e le basi sono gia` state
costruite).  Quindi, se un costruttore fa una significativa
mole di lavoro, deve proteggersi esplicitamente dalla
possibilita` di eccezioni (per garantire il corretto rilascio
di qualsiasi risorsa acquisita sino a quel punto, eccetera
eccetera).  Idealmente, una "classe di rilevanza alla
logica applicativa" dovrebbe potersi concentrare, appunto,
sulla logica applicativa stessa, "delegando" (in senso lato)
a classi di utility (che non abbiano, a loro volta, molti altri
compiti) la gestione richiesta dalla meccanica del linguaggio
e aliena alle problematiche specifiche dell'applicazione.
I contenitori della libreria standard C++, particolarmente
std::string e std::vector<>, sono ideali a questo scopo, e
perfettamente adeguati come "classi membro" cui lasciare
tutte le problematiche "meccaniche" di gestione (fra cui
la costruzione per copia) in una buona maggioranza dei
casi.  Altre classi di utility non previste in libreria standard,
come un template di smart-pointer con reference-count e
magari copy-on-write, sono "mattoncini" molto utili per lo
stesso scopo.
Esistono, in effetti, problematiche applicative che richiedono
di definire esplicitamente un copy-constructor (e allora, quasi
sempre, anche un operatore di copy-assignment, e un
esplicito distruttore), ma, usando bene le feature del C++
standard, e arricchendo la libreria standard con piccole
classi di utility nella propria libreria personale (spesso,
queste piccole utility saranno dei template), questi casi
diventano abbastanza rari.
Vediamo un esempio.  Supponiamo che la mia classe
Pippo debba tenere-per-puntatore un oggetto della mia
classe Pluto, e uno della mia classe Paperino; la
necessita` di tenere, specificamente, per-puntatore,
viene dal fatto che, per ogni dato specifico Pippo, puo`
mancare ("essere nullo il puntatore a") il Pluto, ovvero
il Paperino, o, anche, entrambi.  Le classi Pluto e
Paperino sono gia` scritte, e canoniche; supponiamo
per semplicita` espositiva che, per entrambi, il ctor
non di copia richieda un singolo argomento, di tipo int.
Il primo approccio che viene in mente potrebbe certo
essere:
    class Pippo {
        std::auto_ptr<Pluto> pPluto;
        std::auto_ptr<Paperino> pPape;
    public:
        Pippo(bool hasPluto, bool hasPape,
            int iPluto, int iPape):
                pPluto(hasPluto?new Pluto(iPluto):0),
                pPape(hasPape?new Pape(iPape):0)
        {}
        // ...
    }
Questo e` gia` un approccio abbastanza buono,
in quanto std::auto_ptr si prende cura dell'eventuale
delete del puntatore che incapsula nel _proprio_
dtor.  Tuttavia, questo richiede la scrittura di un
copy ctor per Pippo, in quanto il copy ctor del
template std::auto_ptr ha una semantica di
*trasferimento di proprieta`* che normalmente
non si applica a classi "applicative" come Pippo:
Pippo::Pippo(const Pippo& p):
    pPluto(p.pPluto?new Pluto(*p.pPluto):0),
    pPape(p.pPape?new Paperino(*p.pPape):0)
{}
Non c'e` male, e la migliore indicazione di questo
e` che il _corpo_ del costruttore e` vuoto.  Tuttavia,
la quantita` di logica (duplicata!) nella ctor initializer
list non e` perfettamente rassicurante.  Perche`
devo duplicare tutta questa logica di basso livello
in ogni classe applicativa che deve tenere per
puntatore oggetti di classi simili a Pluto e Paperino?
Non sarebbe meglio che io avessi, una volta per
tutte, definito un mio piccolo template di utility del
tipo...:
template<class Held>
class myptr {
    Held* pHeld;
public:
    template<class Ctorparm>
    myptr(bool any, const Ctorparm& c):
        pHeld(any?new Held(c):0)
    {}
    // piu` eventuali altri ctor comodi, piu`,
    // in particolare, il copy ctor:
    myptr(const myptr& m):
        pHeld(m.pHeld?new Held(*m.pHeld):0)
    {}
    // importante il dtor:
    ~myptr() { delete pHeld; }
    // e l'assegnamento per copia:
    const myptr& operator=(const myptr& m) {
        delete pHeld;
        pHeld = m.pHeld?new Held(*m.pHeld):0;
    }
    // piu` i vari ovvi accessori tipo:
    Held* operator->() { return pHeld; }
    // ecc ecc, come std::auto_ptr
};
(si potrebbe anche costruirlo ereditando da std::auto_ptr,
ma non e` l'approccio piu` pulito; se mai, si potrebbe
"raccogliere a fattor comune", in una piccola function
inline, l'espressione "m.pHeld?<ecc ecc>", comune,
come spesso accade, a copy-ctor e copy-assignment).
Come risultato, la Pippo diverrebbe...:
    class Pippo {
        myptr<Pluto> pPluto;
        myptr<Paperino> pPape;
    public:
        Pippo(bool hasPluto, bool hasPape,
            int iPluto, int iPape):
                pPluto(hasPluto,iPluto),
                pPape(hasPape,iPape)
        {}
        // ...
    };
SENZA piu` bisogno di un copy ctor; inoltre, si
semplifica in modo importante la ctor initializer
list -- tutta la logica e` delegata al template di
utility myptr<>, come appunto si desidera.
[Attenzione: questo template di esempio non e`
adatto a mantenere oggetti polimorfici attraverso
un puntatore-a-base, poiche` causerebbe slicing
alla copia e all'assignment, come d'altronde la
versione originale di Pippo; per evitare lo slicing
occorre il solito trucco -- ad esempio, al posto di
    new Held(*m.pHeld)
usare la chiamata ad un template inline:
    copyctor(*m.pHeld)
con una definizione generica di:
template<class T> inline
T* copyctor(const T&t) {
    return new T(t);
};
ma specializzabile ove serva, ad esempio:
template<> inline
Clonabile* copyctor(const Clonabile&ci) {
    return ci.clone();
};
ecc ecc -- non rilevante al tema del post, eccetto
per notare che anche qui ci sono vantaggi ad aver
usato un template di utility invece che avere i vari
usi di new sparsi qua e la nel codice di rilevanza
alla logica applicativa -- manutenzione + facile!-)].
Da aggiungere alla checklist per una design review
di basso livello, dunque: "Sto facendo uso ottimale
delle possibilita` di libreria standard e classi di utility
per l'infrastruttura di progetto sotto il livello della vera
logica applicativa?  Indicatori cui porre attenzione:
classi di livello ``logica applicativa'' che definiscono
un copy ctor invece di poter delegare la copia ai loro
membri e basi".
Alex