Esercizio - Costruttore di default, di copia e di spostamento (terzo appello 2021-22, teoria)
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
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
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.
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, unvectorè vuoto); se la classe ha altri costruttori e non lo si scrive, il compilatore non lo genera. Si chiama conT a;oT 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 constd::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 | |
| 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.
#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) = 42Lettura:
IntBuffer a;costruttore di default: alloca 8intazzerati.IntBuffer b = a;(equivalente aIntBuffer b {a};) costruttore di copia:aè un lvalue.b.set(1, 99)non cambiaa: i buffer sono distinti (a.get(1) = 11,b.get(1) = 99).IntBuffer c = std::move(a);costruttore di spostamento:std::movetrasformaain rvalue.aora è vuoto (buf_ == nullptr) echa i dati dia.IntBuffer d = costruisci();stampa solodefault: il compilatore applica la copy elision e costruisce l'oggetto locale dicostruiscidirettamente ind, 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_listpermette 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. CopiaT(const T&): copia di un oggetto esistente; di default membro a membro (shallow per i puntatori) si scrive per la deep copy. SpostamentoT(T&&) noexcept: ruba le risorse di un temporaneo o distd::move(x); sorgente vuota.
| default | copia | spostamento | |
|---|---|---|---|
| argomento | nessuno | const T& |
T&& (non const) |
| sorgente | — | invariata | svuotata |
| costo | piccolo | dati | |
| chiamato da | T a; |
T b = a;, T b {a}; |
T c = std::move(a);, ritorno di temporaneo |
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;default;IntBuffer b = a;copia (buffer indipendenti:a.get(1) = 11,b.get(1) = 99);IntBuffer c = std::move(a);spostamento (avuoto);IntBuffer d = costruisci();solodefault(copy elision). - Errori tipici: copia che copia il puntatore (doppia
delete[]); spostamento che non azzera la sorgente;constinT&&; dimenticare il distruttore; credere che il default esista sempre. - Altre chiamate:
per_valore(IntBuffer x)conper_valore(a)costruttore di copia (parametro per valore);vector<IntBuffer>::push_back(a)copia,push_back(std::move(a))sposta; senzanoexceptsul costruttore di spostamento ilvectorpreferirebbe la copia quando rialloca. - Distruttore:
delete[] buf_;anche pernullptr(sorgente spostata); gli assegnamenti di copia e di spostamento completano la regola del cinque.