Esercizio - La classe template DynamicStorage (secondo appello 2022-23, programmazione)
In questa pagina 3
Prova di programmazione (60 minuti) del secondo appello 2022-23 del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. Teoria: C++ - template e programmazione genericaun template permette di usare un tipo (o un intero noto in compilazione) come parametro di una classe o di una funzione; il compilatore genera il codice per ogni tipo per cui il template è specializzato (template instantiation, a tempo di compilazione): il codice è efficiente ma gli errori sono lunghi e oscuri, e il compilatore controlla sia il template sia il suo argomento; la notazione è template
Testo. Implementare la classe template
DynamicStorageche contiene un vettore allocato dinamicamente di elementi di tipoTe di dimensione fissaN(siaTcheNsono parametri template). La classe deve essere dotata di tutte le funzioni necessarie per una corretta gestione della memoria e per rendere compilabili le istruzioni seguenti:std::vector< DynamicStorage<int, 10> > vds1 { DynamicStorage<int, 10>(5) }; std::vector< DynamicStorage<int, 10> > vds2 = vds1;
Che cosa chiedono le due istruzioni
| istruzione | che cosa richiede alla classe |
|---|---|
DynamicStorage<int, 10>(5) |
un costruttore con un argomento (5): il valore con cui si riempiono i 10 elementi |
vector<...> vds1 { X } |
costruisce un vector da una initializer_list con un elemento: gli elementi di una lista sono copiati nel vettore costruttore di copia |
vector<...> vds2 = vds1; |
copia tutto il vector: copia ogni elemento ancora il costruttore di copia, e deve essere una copia profonda (altrimenti due oggetti condividono il buffer) |
Quindi servono: un costruttore con valore, il distruttore (il buffer è dinamico), il costruttore di copia (deep). Per la regola del cinque anche l'assegnamento di copia e le due operazioni di spostamento; il costruttore di spostamento noexcept serve in più perché un vector che si riallocherebbe (push_back) sposta gli elementi invece di copiarli solo se lo spostamento non può lanciare.
Scelte
- Parametri:
template <typename T, std::size_t N>.Ndeve essere noto in compilazione:static_assert(N > 0)lo controlla;size()èstatic constexprperché la dimensione sta nel tipo. - Dato: un solo
T* data_, allocato connew T[N]. Gli elementi diTdevono essere costruibili di default e assegnabili (requisiti suT). - Costruttore:
explicit DynamicStorage(const T& value = T{})riempie convalue; con il valore di defaultT{}serve anche da costruttore di default.explicitimpedisce la conversione implicita daTaDynamicStorage; la scritturaDynamicStorage<int, 10>(5)è una chiamata esplicita, quindi lecita. - Copia profonda: nuovo buffer e
std::copy. Nell'assegnamento si alloca prima e si libera dopo (senewlancia, l'oggetto resta intatto), con il controllothis != &other. - Spostamento: ruba
data_e metteother.data_ = nullptr; il distruttoredelete[] data_funziona anche connullptr. - Definizione delle funzioni: qui in-class (per brevità); nell'esame, se richiesto, vanno fuori classe nello stesso header come nel laboratorio 5.
Soluzione
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <utility>
#include <vector>
// Contenitore di N elementi di tipo T, con il buffer allocato dinamicamente (sia T sia N sono parametri template).
// Invarianti: data_ punta sempre a un buffer di N elementi, tranne dopo uno spostamento (data_ == nullptr).
template <typename T, std::size_t N>
class DynamicStorage {
static_assert(N > 0, "N deve essere positivo"); // controllo a tempo di compilazione
public:
// Costruttore: tutti gli N elementi valgono "value". explicit: niente conversione implicita da T.
// Con il valore di default T{} funziona anche da costruttore di default.
explicit DynamicStorage(const T& value = T{}) : data_{new T[N]}
{
std::fill(data_, data_ + N, value);
}
// Costruttore di COPIA: nuovo buffer e copia degli elementi (richiesto da vector{...} e da vds2 = vds1)
DynamicStorage(const DynamicStorage& other) : data_{new T[N]}
{
std::copy(other.data_, other.data_ + N, data_);
}
// Costruttore di SPOSTAMENTO: ruba il buffer (noexcept: cosi' vector sposta invece di copiare quando cresce)
DynamicStorage(DynamicStorage&& other) noexcept : data_{other.data_}
{
other.data_ = nullptr;
}
// Assegnamento di copia: alloca e copia PRIMA di liberare
DynamicStorage& operator=(const DynamicStorage& other)
{
if (this != &other) {
T* nuovo = new T[N];
std::copy(other.data_, other.data_ + N, nuovo);
delete[] data_;
data_ = nuovo;
}
return *this;
}
// Assegnamento di spostamento
DynamicStorage& operator=(DynamicStorage&& other) noexcept
{
if (this != &other) {
delete[] data_;
data_ = other.data_;
other.data_ = nullptr;
}
return *this;
}
~DynamicStorage() { delete[] data_; } // delete[] su nullptr non fa nulla
T& operator[](std::size_t i) { return data_[i]; }
const T& operator[](std::size_t i) const { return data_[i]; }
static constexpr std::size_t size() { return N; } // la dimensione e' nel tipo
private:
T* data_;
};
int main()
{
// le due istruzioni richieste dal testo
std::vector< DynamicStorage<int, 10> > vds1 { DynamicStorage<int, 10>(5) };
std::vector< DynamicStorage<int, 10> > vds2 = vds1;
vds2[0][3] = 99; // la copia e' PROFONDA: vds1 non cambia
std::cout << "vds1[0][3] = " << vds1[0][3] << ", vds2[0][3] = " << vds2[0][3] << '\n';
std::cout << "size = " << DynamicStorage<int, 10>::size() << ", vds1[0][9] = " << vds1[0][9] << '\n';
vds1.push_back(DynamicStorage<int, 10>(7)); // il vector cresce: sposta gli elementi esistenti
std::cout << "dopo push_back: vds1.size() = " << vds1.size() << ", vds1[1][0] = " << vds1[1][0]
<< ", vds1[0][0] = " << vds1[0][0] << '\n';
DynamicStorage<double, 4> d; // il costruttore con valore di default: zeri
DynamicStorage<double, 4> e(2.5);
d = e; // assegnamento di copia
e = DynamicStorage<double, 4>(1.5); // assegnamento di spostamento
std::cout << "d[2] = " << d[2] << ", e[2] = " << e[2] << '\n';
return 0;
}vds1[0][3] = 5, vds2[0][3] = 99
size = 10, vds1[0][9] = 5
dopo push_back: vds1.size() = 2, vds1[1][0] = 7, vds1[0][0] = 5
d[2] = 2.5, e[2] = 1.5Lettura:
- le due istruzioni richieste compilano: la prima costruisce un oggetto con tutti 5 e lo copia nel
vector(per la lista di inizializzazione), la seconda copia il vettore; vds2[0][3] = 99;non toccavds1[0][3](resta 5): la copia è profonda;size()vale 10 per ogniDynamicStorage<int, 10>;vds1[0][9] = 5: il costruttore riempie tutti gli elementi;vds1.push_back(DynamicStorage<int, 10>(7))porta il vettore a 2 elementi: nella riallocazione il vecchio elemento è spostato (o copiato) nel nuovo buffer evds1[0][0]è ancora 5;- con
doublee :d = e(assegnamento di copia) dàd[2] = 2.5;e = DynamicStorage<double, 4>(1.5)è un assegnamento di spostamento da un temporaneo:e[2] = 1.5.
Versione ripasso
- Che cosa servono le due istruzioni:
DynamicStorage<int, 10>(5)costruttore con un valore (riempie i 10 elementi);vector<...> vds1 { X }evds2 = vds1costruttore di copia profondo. Quindi: costruttore, distruttore, copia, assegnamento di copia, spostamentonoexcept(regola del cinque; ilvectorsposta solo se non può lanciare). - Soluzione:
template <typename T, std::size_t N>
class DynamicStorage {
static_assert(N > 0, "N deve essere positivo");
public:
explicit DynamicStorage(const T& value = T{}) : data_{new T[N]} { std::fill(data_, data_ + N, value); }
DynamicStorage(const DynamicStorage& o) : data_{new T[N]} { std::copy(o.data_, o.data_ + N, data_); }
DynamicStorage(DynamicStorage&& o) noexcept : data_{o.data_} { o.data_ = nullptr; }
DynamicStorage& operator=(const DynamicStorage& o)
{ if (this != &o) { T* n = new T[N]; std::copy(o.data_, o.data_ + N, n); delete[] data_; data_ = n; }
return *this; }
DynamicStorage& operator=(DynamicStorage&& o) noexcept
{ if (this != &o) { delete[] data_; data_ = o.data_; o.data_ = nullptr; } return *this; }
~DynamicStorage() { delete[] data_; }
T& operator[](std::size_t i) { return data_[i]; }
static constexpr std::size_t size() { return N; }
private: T* data_;
};- Prova: le due istruzioni compilano;
vds2[0][3] = 99non cambiavds1[0][3] = 5(copia profonda);size() = 10,vds1[0][9] = 5; dopopush_back(DynamicStorage<int, 10>(7)):vds1.size() = 2,vds1[1][0] = 7,vds1[0][0] = 5;double:d = ed[2] = 2.5;e = DynamicStorage<double, 4>(1.5)e[2] = 1.5. - Errori tipici: copiare il puntatore (shallow) doppia
delete[]; costruttore di spostamento senzanoexcept;explicitmancante oTper valore; assegnamento che libera prima di copiare;Ncome variabile (deve essere costante di compilazione).