Salta al contenuto
Note per Studenti Esercizio - La classe template DynamicStorage (secondo appello 2022-23, programmazione)

Esercizio - La classe template DynamicStorage (secondo appello 2022-23, programmazione)

Esame
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 (o class T, equivalenti) davanti alla classe o alla funzione, e le funzioni membro fuori dalla classe si scrivono template tipo Classe::nome(...); nelle function template il compilatore deduce gli argomenti dai parametri della chiamata (myMax(i, j) invece di myMax(i, j)), ma due argomenti di tipo diverso per lo stesso T sono un errore; una specializzazione esplicita (template<> ...) fornisce il codice per un tipo particolare; i parametri interi (template<typename T, int N> struct array) danno array di dimensione fissa senza free store, più efficienti e che esprimono concetti come la tripletta RGB; poiché il compilatore deve vedere la definizione completa al punto d'uso, le definizioni si mettono nell'header (spesso in un file .hpp incluso in coda al .h).C++ - template e programmazione generica →, 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 →; un esercizio affine è Esercizio - MyVector 2.0 con template (laboratorio 5).

Testo. Implementare la classe template DynamicStorage che contiene un vettore allocato dinamicamente di elementi di tipo T e di dimensione fissa N (sia T che N sono 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 ⇒\Rightarrow costruttore di copia
vector<...> vds2 = vds1; copia tutto il vector: copia ogni elemento ⇒\Rightarrow 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>. N deve essere noto in compilazione: static_assert(N > 0) lo controlla; size() è static constexpr perché la dimensione sta nel tipo.
  • Dato: un solo T* data_, allocato con new T[N]. Gli elementi di T devono essere costruibili di default e assegnabili (requisiti su T).
  • Costruttore: explicit DynamicStorage(const T& value = T{}) riempie con value; con il valore di default T{} serve anche da costruttore di default. explicit impedisce la conversione implicita da T a DynamicStorage; la scrittura DynamicStorage<int, 10>(5) è una chiamata esplicita, quindi lecita.
  • Copia profonda: nuovo buffer e std::copy. Nell'assegnamento si alloca prima e si libera dopo (se new lancia, l'oggetto resta intatto), con il controllo this != &other.
  • Spostamento: ruba data_ e mette other.data_ = nullptr; il distruttore delete[] data_ funziona anche con nullptr.
  • Definizione delle funzioni: qui in-class (per brevità); nell'esame, se richiesto, vanno fuori classe nello stesso header come nel laboratorio 5.

Soluzione

dynamic_storage.cppcpp
#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.5

Lettura:

  • 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 tocca vds1[0][3] (resta 5): la copia è profonda;
  • size() vale 10 per ogni DynamicStorage<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 e vds1[0][0] è ancora 5;
  • con double e N=4N = 4: 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) ⇒\Rightarrow costruttore con un valore (riempie i 10 elementi); vector<...> vds1 { X } e vds2 = vds1 ⇒\Rightarrow costruttore di copia profondo. Quindi: costruttore, distruttore, copia, assegnamento di copia, spostamento noexcept (regola del cinque; il vector sposta solo se non può lanciare).
  • Soluzione:
cpp
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] = 99 non cambia vds1[0][3] = 5 (copia profonda); size() = 10, vds1[0][9] = 5; dopo push_back(DynamicStorage<int, 10>(7)): vds1.size() = 2, vds1[1][0] = 7, vds1[0][0] = 5; double: d = e →\to d[2] = 2.5; e = DynamicStorage<double, 4>(1.5) →\to e[2] = 1.5.
  • Errori tipici: copiare il puntatore (shallow) ⇒\Rightarrow doppia delete[]; costruttore di spostamento senza noexcept; explicit mancante o T per valore; assegnamento che libera prima di copiare; N come variabile (deve essere costante di compilazione).

Esercizi su questo argomento

Teoria collegata