Salta al contenuto
Note per Studenti Esercizio - Costruttore di default, di copia e di spostamento (quarto appello 2022-23, teoria)

Esercizio - Costruttore di default, di copia e di spostamento (quarto appello 2022-23, teoria)

Esame

Domanda aperta 1 del quarto appello 2022-23 del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. È la stessa domanda del terzo appello 2021-22: Esercizio - Costruttore di default, di copia e di spostamento (terzo appello 2021-22, teoria), dove la definizione e il confronto sono sviluppati con un'altra classe; qui la classe è diversa e si mostra anche che cosa cambia disattivando la copy elision. 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 permette la lista {a, b, c} e con le graffe ha la precedenza sul costruttore a un intero (vector{3} è un elemento, vector(3) sono tre); la copia di default è membro a membro (shallow copy) e con un puntatore a un buffer duplica il puntatore invece del contenuto (due oggetti che condividono i dati e una doppia delete), quindi servono il costruttore di copia e l'assegnamento di copia che fanno la deep copy (l'assegnamento prima alloca e copia, poi libera i vecchi dati, e deve tollerare l'auto-assegnamento); this è il puntatore all'oggetto corrente, parametro implicito delle funzioni membro; il costruttore e l'assegnamento di spostamento (T&&, rvalue reference) rubano il buffer della sorgente lasciandola vuota ma valida, il compilatore li sceglie da solo (con la copy elision a volte non servono nemmeno); i cinque (distruttore, copia, spostamento) vanno definiti insieme.C++ - costruttori, inizializzazione, copia e spostamento →.

Testo. Si consideri una classe in C++. Si spieghi cosa si intende per: a) costruttore di default; b) costruttore di copia; c) costruttore di spostamento. Si mettano in luce le differenze tra i tre costruttori. Si crei inoltre una semplice classe con un solo dato membro (puntatore a un buffer di int allocato dinamicamente), dotata dei tre costruttori e del distruttore, opportunamente implementati. Si scrivano, infine, spezzoni di codice che provocano la chiamata dei tre costruttori elencati sopra.

Definizioni e differenze

a) default b) copia c) spostamento
definizione si chiama senza argomenti e crea un oggetto in uno stato iniziale valido crea un oggetto come copia profonda di uno esistente crea un oggetto rubando le risorse di uno che sta per sparire
firma T() T(const T&) T(T&&) noexcept
sorgente non c'è resta invariata (const) resta in stato valido ma vuoto (non const)
risorse ne acquisisce di nuove ne duplica (nuovo buffer + copia degli elementi) le trasferisce (copia solo il puntatore e annulla quello della sorgente)
costo piccolo proporzionale ai dati costante, O(1)O(1)
chi lo chiama T x;, T x {};, new T T y = x;, T y {x};, passaggio per valore, ritorno di un oggetto non temporaneo (se non elisa) T z = std::move(x);, ritorno/passaggio di un temporaneo
se non scritto generato solo se nessun altro costruttore è dichiarato generato: copia membro a membro (shallow per i puntatori) generato solo se non sono dichiarati copia, assegnamenti o distruttore

La copia di default su un puntatore copia l'indirizzo: due oggetti con lo stesso buffer e due delete[] sullo stesso blocco. Per questo una classe con memoria dinamica scrive la copia profonda, e lo spostamento permette di evitare la copia quando la sorgente non serve più (il valore di ritorno di una funzione, un std::move).

La classe IntBlock

Un solo dato membro int* buf_; la lunghezza è una costante della classe. Distruttore, copia e spostamento rispettano il possesso del buffer; gli assegnamenti completano la regola del cinque.

intblock.cppcpp
#include <algorithm>
#include <iostream>
#include <utility>
using namespace std;

// Classe con UN solo dato membro: il puntatore a un buffer di int allocato dinamicamente.
class IntBlock {
public:
    // a) COSTRUTTORE DI DEFAULT: nessun argomento, stato iniziale valido (buffer di kLen zeri)
    IntBlock() : buf_{new int[kLen]()} { cout << "  [default]\n"; }

    // b) COSTRUTTORE DI COPIA: nuovo buffer + copia degli elementi (deep copy); l'originale non cambia
    IntBlock(const IntBlock& o) : buf_{new int[kLen]}
    {
        copy(o.buf_, o.buf_ + kLen, buf_);
        cout << "  [copia]\n";
    }

    // c) COSTRUTTORE DI SPOSTAMENTO: prende il buffer, la sorgente resta vuota (nullptr)
    IntBlock(IntBlock&& o) noexcept : buf_{o.buf_}
    {
        o.buf_ = nullptr;
        cout << "  [spostamento]\n";
    }

    ~IntBlock() { delete[] buf_; }                           // libera (nullptr incluso)

    IntBlock& operator=(const IntBlock& o)                    // gli assegnamenti completano la regola del cinque
    {
        if (this != &o) { int* n = new int[kLen]; copy(o.buf_, o.buf_ + kLen, n); delete[] buf_; buf_ = n; }
        return *this;
    }
    IntBlock& operator=(IntBlock&& o) noexcept
    {
        if (this != &o) { delete[] buf_; buf_ = o.buf_; o.buf_ = nullptr; }
        return *this;
    }

    void set(int i, int v) { buf_[i] = v; }
    int get(int i) const { return buf_[i]; }
    bool vuoto() const { return buf_ == nullptr; }

private:
    static constexpr int kLen = 5;
    int* buf_;
};

IntBlock crea()
{
    IntBlock b;
    b.set(0, 7);
    return b;
}

void per_valore(IntBlock x)                                  // il parametro per valore e' costruito con la COPIA
{
    x.set(0, 123);
}

int main()
{
    cout << "IntBlock a;\n";
    IntBlock a;
    a.set(0, 1);
    cout << "IntBlock b {a};\n";
    IntBlock b {a};                                          // copia (forma con graffe)
    cout << "per_valore(a);\n";
    per_valore(a);                                           // copia nel parametro
    cout << "  a.get(0) = " << a.get(0) << " (invariato)\n";
    cout << "IntBlock c = std::move(a);\n";
    IntBlock c = std::move(a);                               // spostamento
    cout << "  a vuoto? " << a.vuoto() << ", c.get(0) = " << c.get(0) << '\n';
    cout << "IntBlock d = crea();\n";
    IntBlock d = crea();                                     // temporaneo -> spostamento (o elisione)
    cout << "  d.get(0) = " << d.get(0) << '\n';
    (void)b;
    return 0;
}
IntBlock a;
  [default]
IntBlock b {a};
  [copia]
per_valore(a);
  [copia]
  a.get(0) = 1 (invariato)
IntBlock c = std::move(a);
  [spostamento]
  a vuoto? 1, c.get(0) = 1
IntBlock d = crea();
  [default]
  d.get(0) = 7

Le chiamate, nell'ordine:

  1. IntBlock a; →\to costruttore di default ([default]).
  2. IntBlock b {a}; →\to costruttore di copia ([copia]): a è un lvalue.
  3. per_valore(a); →\to costruttore di copia ([copia]): il parametro per valore è costruito con una copia; l'x.set(0, 123) dentro la funzione agisce sulla copia (a.get(0) = 1 invariato).
  4. IntBlock c = std::move(a); →\to costruttore di spostamento ([spostamento]): std::move rende a un rvalue; a ora è vuoto e c ha i dati.
  5. IntBlock d = crea(); →\to stampa soltanto [default]: la copy elision costruisce l'oggetto direttamente in d.

Compilando con -fno-elide-constructors (che disattiva l'ottimizzazione) l'ultima riga diventa:

IntBlock d = crea();
  [default]
  [spostamento]
  d.get(0) = 7

cioè il valore di ritorno del temporaneo è spostato in d: è il caso tipico per cui lo spostamento esiste, e la ragione per cui restituire per valore un oggetto grande è economico.

Versione ripasso

default copia spostamento
firma T() T(const T&) T(T&&) noexcept
fa stato iniziale valido copia profonda di un oggetto esistente ruba le risorse di un oggetto che sta per sparire
sorgente — invariata valida ma vuota (non const)
costo piccolo ∝\propto dati O(1)O(1)
chiamato da T x;, new T T y = x;, T y {x};, passaggio per valore T z = std::move(x);, temporaneo
se non scritto generato solo senza altri costruttori membro a membro (shallow per i puntatori) generato solo senza copia/assegnamenti/distruttore
cpp
class IntBlock {
public:
    IntBlock() : buf_{new int[kLen]()} {}
    IntBlock(const IntBlock& o) : buf_{new int[kLen]} { copy(o.buf_, o.buf_ + kLen, buf_); }
    IntBlock(IntBlock&& o) noexcept : buf_{o.buf_} { o.buf_ = nullptr; }
    ~IntBlock() { delete[] buf_; }
private:
    static constexpr int kLen = 5;
    int* buf_;
};
  • Chiamate: IntBlock a; →\to [default]; IntBlock b {a}; e per_valore(a); →\to [copia] (la funzione agisce sulla copia: a.get(0) = 1); IntBlock c = std::move(a); →\to [spostamento], a vuoto; IntBlock d = crea(); →\to solo [default] (copy elision); con -fno-elide-constructors: [default], [spostamento].
  • Errori tipici: copia che copia il puntatore; spostamento che non annulla la sorgente; const in T&&; credere che T y = x; sia un assegnamento (è il costruttore di copia); dimenticare il distruttore.
  • Costruttore a una riga per tipo: default T x; stato valido; copia T y = x; nuovo buffer + elementi; spostamento T z = std::move(x); prende il puntatore e azzera quello della sorgente. Per un IntBlock di kLen = 5: a.set(0, 1), b {a} →\to b ha una copia, c = std::move(a) →\to a.vuoto() = 1, c.get(0) = 1; crea() ritorna un IntBlock con get(0) = 7.

Esercizi su questo argomento

Teoria collegata