Esercizio - Costruttore di default, di copia e di spostamento (quarto appello 2022-23, teoria)
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
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
intallocato 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, |
| 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.
#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) = 7Le chiamate, nell'ordine:
IntBlock a;costruttore di default ([default]).IntBlock b {a};costruttore di copia ([copia]):aè un lvalue.per_valore(a);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) = 1invariato).IntBlock c = std::move(a);costruttore di spostamento ([spostamento]):std::moverendeaun rvalue;aora è vuoto echa i dati.IntBlock d = crea();stampa soltanto[default]: la copy elision costruisce l'oggetto direttamente ind.
Compilando con -fno-elide-constructors (che disattiva l'ottimizzazione) l'ultima riga diventa:
IntBlock d = crea();
[default]
[spostamento]
d.get(0) = 7cioè 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 | dati | |
| 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 |
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;[default];IntBlock b {a};eper_valore(a);[copia](la funzione agisce sulla copia:a.get(0) = 1);IntBlock c = std::move(a);[spostamento],avuoto;IntBlock d = crea();solo[default](copy elision); con-fno-elide-constructors:[default],[spostamento]. - Errori tipici: copia che copia il puntatore; spostamento che non annulla la sorgente;
constinT&&; credere cheT y = x;sia un assegnamento (è il costruttore di copia); dimenticare il distruttore. - Costruttore a una riga per tipo: default
T x;stato valido; copiaT y = x;nuovo buffer + elementi; spostamentoT z = std::move(x);prende il puntatore e azzera quello della sorgente. Per unIntBlockdikLen = 5:a.set(0, 1),b {a}bha una copia,c = std::move(a)a.vuoto() = 1,c.get(0) = 1;crea()ritorna unIntBlockconget(0) = 7.