Salta al contenuto
Note per Studenti Esercizio - Costruttore di default, di copia e di spostamento (terzo appello 2021-22, teoria)

Esercizio - Costruttore di default, di copia e di spostamento (terzo appello 2021-22, teoria)

Esame

Domanda aperta 3 del terzo appello 2021-22 (30 giugno 2022) del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. La stessa domanda è ricomparsa nel quarto appello 2022-23 (Esercizio - Costruttore di default, di copia e di spostamento (quarto appello 2022-23, teoria)). 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. 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.

I tre costruttori

a) Costruttore di default. Costruttore che si può chiamare senza argomenti. Dà all'oggetto uno stato iniziale valido. Esiste solo se c'è un valore di default sensato (una string è vuota, un vector è vuoto); se la classe ha altri costruttori e non lo si scrive, il compilatore non lo genera. Si chiama con T a; o T a {};.

b) Costruttore di copia. T(const T& other): crea un oggetto come copia di un altro esistente, che resta invariato. Il comportamento generato dal compilatore è la copia membro a membro; con un puntatore a memoria dinamica copierebbe l'indirizzo (shallow copy), perciò va scritto a mano per fare una deep copy: allocare un nuovo buffer e copiare gli elementi.

c) Costruttore di spostamento. T(T&& other) noexcept: crea un oggetto rubando le risorse di un oggetto che sta per sparire (un temporaneo o un oggetto marcato con std::move); la sorgente resta in uno stato valido ma vuoto.

default copia spostamento
argomenti nessuno const T& (lvalue) T&& (rvalue), non const
che cosa fa stato iniziale duplica i dati (deep) trasferisce il possesso
sorgente dopo non c'è invariata svuotata (nullptr)
costo piccolo proporzionale ai dati O(1)O(1)
chiamato da T a; T b = a;, T b {a};, argomento per valore T c = std::move(a);, ritorno di un temporaneo
se non scritto generato solo se non ci sono altri costruttori generato membro a membro generato solo se non ci sono copia/distruttore dichiarati

La classe IntBuffer

Un solo dato membro: int* buf_ (la lunghezza è una costante della classe). Il distruttore libera il buffer; copia e spostamento rispettano il possesso del buffer.

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

// Una classe con UN solo dato membro: il puntatore a un buffer di int allocato dinamicamente.
// (La lunghezza e' una costante della classe, cosi' il dato membro resta uno solo.)
class IntBuffer {
public:
    // 1. COSTRUTTORE DI DEFAULT: chiamabile senza argomenti, crea uno stato iniziale valido (buffer di zeri)
    IntBuffer() : buf_{new int[kN]()}                       // () azzera
    {
        cout << "  default\n";
    }

    // 2. COSTRUTTORE DI COPIA: crea un NUOVO buffer e copia gli elementi (deep copy)
    IntBuffer(const IntBuffer& other) : buf_{new int[kN]}
    {
        copy(other.buf_, other.buf_ + kN, buf_);
        cout << "  copia\n";
    }

    // 3. COSTRUTTORE DI SPOSTAMENTO: ruba il buffer della sorgente, che resta vuota
    IntBuffer(IntBuffer&& other) noexcept : buf_{other.buf_}
    {
        other.buf_ = nullptr;
        cout << "  spostamento\n";
    }

    // distruttore: libera il buffer (delete[] su nullptr non fa nulla)
    ~IntBuffer() { delete[] buf_; }

    // assegnamenti: non richiesti dal testo, ma servono per rispettare la regola del cinque
    IntBuffer& operator=(const IntBuffer& other)
    {
        if (this != &other) {
            int* nuovo = new int[kN];
            copy(other.buf_, other.buf_ + kN, nuovo);
            delete[] buf_;
            buf_ = nuovo;
        }
        return *this;
    }
    IntBuffer& operator=(IntBuffer&& other) noexcept
    {
        if (this != &other) { delete[] buf_; buf_ = other.buf_; other.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 kN = 8;
    int* buf_;
};

IntBuffer costruisci()
{
    IntBuffer b;                // chiamata al costruttore di default
    b.set(0, 42);
    return b;                   // spostato (o elisione) verso il chiamante
}

int main()
{
    cout << "1. IntBuffer a;\n";
    IntBuffer a;                                   // costruttore di default
    a.set(1, 11);

    cout << "2. IntBuffer b = a;   (anche IntBuffer b {a};)\n";
    IntBuffer b = a;                               // costruttore di copia: a e' un lvalue
    b.set(1, 99);
    cout << "  a.get(1) = " << a.get(1) << ", b.get(1) = " << b.get(1) << "  (buffer indipendenti)\n";

    cout << "3. IntBuffer c = std::move(a);\n";
    IntBuffer c = std::move(a);                    // costruttore di spostamento: std::move rende a un rvalue
    cout << "  a vuoto? " << a.vuoto() << ", c.get(1) = " << c.get(1) << '\n';

    cout << "4. IntBuffer d = costruisci();\n";
    IntBuffer d = costruisci();                    // il risultato e' un temporaneo: sposta (o elide)
    cout << "  d.get(0) = " << d.get(0) << '\n';
    return 0;
}
1. IntBuffer a;
  default
2. IntBuffer b = a;   (anche IntBuffer b {a};)
  copia
  a.get(1) = 11, b.get(1) = 99  (buffer indipendenti)
3. IntBuffer c = std::move(a);
  spostamento
  a vuoto? 1, c.get(1) = 11
4. IntBuffer d = costruisci();
  default
  d.get(0) = 42

Lettura:

  1. IntBuffer a; →\to costruttore di default: alloca 8 int azzerati.
  2. IntBuffer b = a; (equivalente a IntBuffer b {a};) →\to costruttore di copia: a è un lvalue. b.set(1, 99) non cambia a: i buffer sono distinti (a.get(1) = 11, b.get(1) = 99).
  3. IntBuffer c = std::move(a); →\to costruttore di spostamento: std::move trasforma a in rvalue. a ora è vuoto (buf_ == nullptr) e c ha i dati di a.
  4. IntBuffer d = costruisci(); →\to stampa solo default: il compilatore applica la copy elision e costruisce l'oggetto locale di costruisci direttamente in d, senza copiare né spostare; in alcuni casi (o con -fno-elide-constructors) si avrebbe lo spostamento dal temporaneo (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 →).

Anche il distruttore delete[] buf_; è necessario e funziona con nullptr. Gli assegnamenti non sono richiesti, ma la regola del cinque li vuole accanto agli altri.

Versione ripasso

  • Default: senza argomenti, stato iniziale valido; T a;; non generato se ci sono altri costruttori. Copia T(const T&): copia di un oggetto esistente; di default membro a membro (shallow per i puntatori) ⇒\Rightarrow si scrive per la deep copy. Spostamento T(T&&) noexcept: ruba le risorse di un temporaneo o di std::move(x); sorgente vuota.
default copia spostamento
argomento nessuno const T& T&& (non const)
sorgente — invariata svuotata
costo piccolo ∝\propto dati O(1)O(1)
chiamato da T a; T b = a;, T b {a}; T c = std::move(a);, ritorno di temporaneo
cpp
class IntBuffer {
public:
    IntBuffer() : buf_{new int[kN]()} {}
    IntBuffer(const IntBuffer& o) : buf_{new int[kN]} { copy(o.buf_, o.buf_ + kN, buf_); }
    IntBuffer(IntBuffer&& o) noexcept : buf_{o.buf_} { o.buf_ = nullptr; }
    ~IntBuffer() { delete[] buf_; }
private:
    static constexpr int kN = 8;
    int* buf_;
};
  • Chiamate: IntBuffer a; →\to default; IntBuffer b = a; →\to copia (buffer indipendenti: a.get(1) = 11, b.get(1) = 99); IntBuffer c = std::move(a); →\to spostamento (a vuoto); IntBuffer d = costruisci(); →\to solo default (copy elision).
  • Errori tipici: copia che copia il puntatore (doppia delete[]); spostamento che non azzera la sorgente; const in T&&; dimenticare il distruttore; credere che il default esista sempre.
  • Altre chiamate: per_valore(IntBuffer x) con per_valore(a) →\to costruttore di copia (parametro per valore); vector<IntBuffer>::push_back(a) copia, push_back(std::move(a)) sposta; senza noexcept sul costruttore di spostamento il vector preferirebbe la copia quando rialloca.
  • Distruttore: delete[] buf_; anche per nullptr (sorgente spostata); gli assegnamenti di copia e di spostamento completano la regola del cinque.

Esercizi su questo argomento

Teoria collegata