Esercizio - Distruttore, copia e spostamento della classe Elements (secondo appello 2021-22, programmazione)
In questa pagina 3
Prova di programmazione (60 minuti) del secondo appello 2021-22 (11 febbraio 2022) del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. 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 la classe:
#include <vector> class Elements { public: Elements() : buffer(new double[kLength]) {} private: int i = 0; double* buffer = nullptr; std::vector<int> values; static constexpr int kLength = 300; };Implementare: a) il distruttore; b) il costruttore di copia e quello di spostamento, l'assegnamento di copia e quello di spostamento. In relazione ad essi: c) motivare le differenze di implementazione tra i due costruttori; d) mostrare un esempio di codice che causa la chiamata dell'assegnamento di copia, e un altro esempio che causa la chiamata dell'assegnamento di spostamento, spiegando il perché.
Analisi dei membri
Per ogni membro bisogna dire che cosa fa la copia (o lo spostamento) di default:
| membro | tipo | copia di default | spostamento di default | basta? |
|---|---|---|---|---|
i |
int |
copia il valore | copia il valore | sì |
values |
std::vector<int> |
copia gli elementi (profonda) | sposta il buffer interno | sì |
buffer |
double* |
copia l'indirizzo (shallow) | copia l'indirizzo, la sorgente continua a puntare | no |
kLength |
static constexpr |
non è un dato dell'oggetto | idem | non conta |
Il problema è buffer: l'unico puntatore nudo, che possiede memoria dinamica (300 double). Per questo la classe ha bisogno del distruttore e delle quattro operazioni, la regola del cinque (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
Soluzione
#include <algorithm>
#include <iostream>
#include <utility>
#include <vector>
using namespace std;
class Elements {
public:
Elements() : buffer(new double[kLength]) {} // costruttore dato dal testo (il buffer non e' inizializzato)
// a) distruttore: libera l'unica risorsa "nuda", il buffer; il vector libera da solo i suoi dati
~Elements() { delete[] buffer; }
// b1) costruttore di copia: deep copy
Elements(const Elements& other)
: i(other.i),
buffer(new double[kLength]), // NUOVO buffer: non si copia il puntatore
values(other.values) // vector: la sua copia e' gia' profonda
{
copy(other.buffer, other.buffer + kLength, buffer);
}
// b2) costruttore di spostamento: ruba il buffer
Elements(Elements&& other) noexcept
: i(other.i),
buffer(other.buffer), // si prende il puntatore...
values(std::move(other.values)) // ...e i dati del vector
{
other.buffer = nullptr; // la sorgente non possiede piu' nulla (delete[] nullptr e' innocuo)
other.i = 0;
}
// b3) assegnamento di copia
Elements& operator=(const Elements& other)
{
if (this != &other) { // auto-assegnamento: niente da fare
double* nuovo = new double[kLength]; // 1. alloca
copy(other.buffer, other.buffer + kLength, nuovo); // 2. copia
delete[] buffer; // 3. libera il vecchio
buffer = nuovo; // 4. sostituisce
i = other.i;
values = other.values;
}
return *this;
}
// b4) assegnamento di spostamento
Elements& operator=(Elements&& other) noexcept
{
if (this != &other) {
delete[] buffer; // libera il proprio buffer
buffer = other.buffer; // ruba quello della sorgente
other.buffer = nullptr;
i = other.i;
other.i = 0;
values = std::move(other.values);
}
return *this;
}
// --- funzioni aggiunte soltanto per provare la classe ---
void set(int k, double x) { buffer[k] = x; }
double get(int k) const { return buffer[k]; }
void add(int v) { values.push_back(v); }
size_t n_values() const { return values.size(); }
bool has_buffer() const { return buffer != nullptr; }
private:
int i = 0;
double* buffer = nullptr;
std::vector<int> values;
static constexpr int kLength = 300;
};
int main()
{
Elements a;
a.set(0, 1.5);
a.add(10); a.add(20);
Elements b = a; // costruttore di copia
b.set(0, 9.9);
cout << "dopo la copia: a.get(0) = " << a.get(0) << ", b.get(0) = " << b.get(0)
<< ", a.n_values = " << a.n_values() << ", b.n_values = " << b.n_values() << '\n';
Elements c = std::move(a); // costruttore di spostamento
cout << "dopo lo spostamento: a.has_buffer = " << a.has_buffer() << ", a.n_values = " << a.n_values()
<< ", c.get(0) = " << c.get(0) << ", c.n_values = " << c.n_values() << '\n';
Elements d;
d = b; // assegnamento di copia (d esiste gia')
d.set(0, 7.7);
cout << "dopo assegnamento di copia: b.get(0) = " << b.get(0) << ", d.get(0) = " << d.get(0) << '\n';
Elements e;
e = Elements(); // assegnamento di spostamento (la sorgente e' un temporaneo)
e = std::move(d); // assegnamento di spostamento (esplicito)
cout << "dopo assegnamento di spostamento: d.has_buffer = " << d.has_buffer() << ", e.get(0) = " << e.get(0) << '\n';
return 0;
}dopo la copia: a.get(0) = 1.5, b.get(0) = 9.9, a.n_values = 2, b.n_values = 2
dopo lo spostamento: a.has_buffer = 0, a.n_values = 0, c.get(0) = 1.5, c.n_values = 2
dopo assegnamento di copia: b.get(0) = 9.9, d.get(0) = 7.7
dopo assegnamento di spostamento: d.has_buffer = 0, e.get(0) = 7.7a) Distruttore. delete[] buffer;: è new[], quindi delete[]. Non si tocca values: il suo distruttore è chiamato dal compilatore. Funziona anche con buffer == nullptr (oggetto spostato): delete[] nullptr non fa nulla.
b) Le quattro operazioni. Il costruttore e l'assegnamento di copia fanno una deep copy: nuovo buffer di kLength double, copia degli elementi con std::copy, e values si copia con la sua copia. L'assegnamento in più deve liberare il vecchio buffer e gestire l'auto-assegnamento; alloca e copia prima di liberare, così se new fallisce l'oggetto resta intatto. Il costruttore e l'assegnamento di spostamento rubano: copiano il puntatore, mettono other.buffer = nullptr e spostano values con std::move. Sono noexcept (permettono ai contenitori di preferire lo spostamento).
c) Differenze fra i due costruttori.
| costruttore di copia | costruttore di spostamento | |
|---|---|---|
| parametro | const Elements& |
Elements&& (rvalue reference, non const: la sorgente si modifica) |
buffer |
alloca un nuovo buffer e copia i 300 double |
prende il puntatore della sorgente, senza allocare né copiare |
| sorgente dopo | invariata | svuotata: buffer = nullptr, i = 0, values vuoto |
| costo | allocazione + copia di 300 double + copia di values |
pochi assegnamenti di puntatori, |
values |
values(other.values) |
values(std::move(other.values)) |
noexcept |
non necessario | sì |
Se lo spostamento copiasse soltanto il puntatore senza azzerare la sorgente, due oggetti avrebbero lo stesso buffer e il distruttore farebbe la delete[] due volte.
d) Chiamate dell'assegnamento. L'assegnamento si usa quando l'oggetto a sinistra esiste già (altrimenti è un costruttore).
Elements d;
d = b; // b è un lvalue (ha un nome e sopravvive): ASSEGNAMENTO DI COPIA
Elements e;
e = Elements(); // il sorgente è un temporaneo (rvalue): ASSEGNAMENTO DI SPOSTAMENTO
e = std::move(d); // std::move trasforma d in rvalue: ASSEGNAMENTO DI SPOSTAMENTOPerché: il compilatore sceglie l'overload in base alla categoria di valore dell'operando destro: un lvalue (b) si lega a const Elements&, quindi copia; un rvalue (un temporaneo, o un lvalue dichiarato "spostabile" con std::move) si lega a Elements&&, quindi sposta. Con std::move(d) il programmatore promette di non usare più il contenuto di d: nel test d.has_buffer = 0 dopo lo spostamento.
Osservazioni
- Il costruttore dato lascia i 300
doublenon inizializzati (new double[kLength]senza()): la copia legge valori indeterminati, formalmente comportamento indefinito per idouble; nella pratica non ha effetti. Basterebbenew double[kLength]()per azzerarli. - Con il costruttore di spostamento, la sorgente resta in uno stato valido ma vuoto: si può distruggere o riassegnare, non usarne i dati.
- Le funzioni
set,get,add,n_values,has_buffersono aggiunte soltanto per provare la classe: l'esame richiede solo le cinque funzioni.
Versione ripasso
- Analisi dei membri:
i(int) evalues(vector<int>) vanno bene di default (ilvectorcopia profondo / sposta il buffer);buffer(double*) copiato di default come indirizzo: shallow servono le cinque funzioni.kLengthèstatic. - a) Distruttore:
~Elements() { delete[] buffer; }(new[]delete[];nullptrinnocuo). - b) Copia (deep):
Elements(const Elements& o) : i(o.i), buffer(new double[kLength]), values(o.values)
{ copy(o.buffer, o.buffer + kLength, buffer); }
Elements& operator=(const Elements& o)
{ if (this != &o) { double* n = new double[kLength]; copy(o.buffer, o.buffer + kLength, n);
delete[] buffer; buffer = n; i = o.i; values = o.values; }
return *this; }Spostamento (rubare):
Elements(Elements&& o) noexcept : i(o.i), buffer(o.buffer), values(std::move(o.values))
{ o.buffer = nullptr; o.i = 0; }
Elements& operator=(Elements&& o) noexcept
{ if (this != &o) { delete[] buffer; buffer = o.buffer; o.buffer = nullptr;
i = o.i; o.i = 0; values = std::move(o.values); }
return *this; }- c) Differenze: copia =
const Elements&, alloca e copia 300double, sorgente invariata, costo alto; spostamento =Elements&&(nonconst), prende il puntatore e annulla la sorgente (nullptr), costo ,noexcept. Senzao.buffer = nullptrdoppiadelete[]. - d) Esempi:
Elements d; d = b;(lvalue copia);e = Elements();(temporaneo spostamento);e = std::move(d);(moverendedun rvalue spostamento). Il compilatore sceglie l'overload dalla categoria di valore dell'operando destro. - Prova: dopo la copia
a.get(0) = 1.5,b.get(0) = 9.9; dopo lo spostamentoa.has_buffer = 0,c.get(0) = 1.5,c.n_values = 2;d.has_buffer = 0dopostd::move(d). - Errori tipici:
deleteal posto didelete[]; non azzerareother.buffernello spostamento; copiare il puntatore nella copia; assegnamento che libera prima di copiare; dimenticarevaluesei. - Osservazione sul testo:
Elements() : buffer(new double[kLength]) {}lascia i 300doublenon inizializzati (formalmente la copia legge valori indeterminati):new double[kLength]()li azzererebbe. Dopo lo spostamento la sorgente è valida ma vuota:buffer == nullptr,valuesvuoto in pratica. - Perché
noexcept:std::vector<Elements>sposta gli elementi in riallocazione solo se lo spostamento non può lanciare.