Esercizio - ConvertToArray da un container STL (secondo appello 2023-24, programmazione)
In questa pagina 3
Testo (60 minuti, su carta). Progettare e implementare la classe ConvertToArray che ha lo scopo di convertire qualsiasi container STL di int in un array allocato dinamicamente. La classe deve avere: un costruttore di default; la funzione membro convert che accetta in input un container STL, alloca un buffer di int di dimensioni adeguate e copia tutti gli elementi del container all'interno del buffer; questa funzione deve essere in grado di gestire qualsiasi container STL; ogni altra funzione necessaria alla buona gestione della memoria. Implementare inoltre un main di test. Nello svolgimento, indicare chiaramente la suddivisione del codice in file.
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
1. Lettura del testo
Tre parole del testo decidono il progetto.
- "qualsiasi container STL":
convertnon può avere un parametrovector<int>; deve essere un template di funzione membro che accetta il tipo del contenitore come parametro (template <typename Container> void convert(const Container&)). Funziona con tutto ciò che habegin()eend():vector,list,deque,set,array,forward_list. È polimorfismo statico (C++ - polimorfismoPolimorfismo = abilità di associare comportamenti diversi a un'unica notazione generica. In C++ ci sono due forme principali: dinamico (ereditarietà più funzioni virtuali: vincolato dall'interfaccia della classe base, binding a run-time, permette collezioni eterogenee) e statico (template: svincolato, basta che i tipi abbiano la stessa sintassi, binding a tempo di compilazione, nessun costo a run-time ma una funzione per ogni tipo); il polimorfismo statico è lo strumento della programmazione generica. Anche l'overloading di funzioni e di operatori può essere visto come polimorfismo. Nella gerarchia si naviga con l'upcasting (implicito, sempre lecito) e il downcasting con dynamic_cast, l'unico cast con verifica a run-time: su un puntatore dà nullptr se non lecito, su un riferimento lancia bad_cast.C++ - polimorfismo →): non serve una classe base comune. Poiché è un template, la sua definizione deve stare nell'header. - "alloca un buffer di dimensioni adeguate": la dimensione si ricava con
std::distance(begin, end), non conc.size(), cheforward_listnon ha. - "buona gestione della memoria": la classe possiede un buffer con
new[], quindi servono le operazioni essenziali (C++ - buona interfaccia e progetti su più fileUna classe C++ ben progettata decide esplicitamente le sette operazioni essenziali (costruttore con argomenti, di default, di copia, assegnamento di copia, costruttore e assegnamento di spostamento, distruttore): se serve il distruttore perché si possiede una risorsa, servono quasi sempre anche copia e spostamento; un costruttore a un argomento definisce una conversione implicita, che si blocca con explicit; per capire quando i costruttori e il distruttore vengono chiamati si traccia una struct che stampa ogni chiamata. Un progetto su più file separa interfaccia (header .h con definizioni di classe e dichiarazioni, protetto da include guard) e implementazione (.cpp, compilati uno per uno, mai inclusi); al compilatore si passano solo i .cpp, il main sta in un file a sé; gli errori di compilazione (sintassi, tipi, dichiarazioni mancanti) vengono dal compilatore, quelli di linking (definizione mancante, due main, nessun main) dal linker.C++ - buona interfaccia e progetti su più file →): distruttore condelete[], copia profonda (costruttore e assegnamento), spostamento. Il testo non le nomina: sono "ogni altra funzione necessaria".
Divisione in file: convert_to_array.h (classe e template di convert), convert_to_array.cpp (le sei funzioni non template), main.cpp (il test).
convert: l'ordine delle operazioni
- si calcola
n; - si alloca il buffer nuovo (
new int[n]) prima di toccare quello vecchio; - si copia con
std::copy; - solo ora
delete[]del vecchio buffer, e si aggiornanodata_esize_.
Se l'allocazione fallisce (bad_alloc) o la copia lancia, l'oggetto è ancora quello di prima: garanzia forte (C++ - eccezioni e gestione delle risorse (RAII)Un errore che non si può gestire dove avviene si segnala con throw e si gestisce con try/catch più in alto; la libreria standard ha la gerarchia std::exception (logic_error e runtime_error con le loro derivate) e le eccezioni proprie si derivano pubblicamente da una di esse; si cattura per riferimento. Le eccezioni mettono in crisi il codice che acquisisce risorse (memoria con new, file, lock, socket): se una chiamata lancia prima della delete, la risorsa si perde. La soluzione è RAII (Resource Acquisition Is Initialization): acquisire la risorsa nel costruttore di un oggetto locale e rilasciarla nel distruttore, perché all'uscita da uno scope, anche per eccezione (stack unwinding), i distruttori degli oggetti locali vengono chiamati. Garanzie di una funzione: basic (nessuna perdita, stato valido), strong (commit or rollback: se fallisce lo stato osservabile è quello di prima) e no-throw (non lancia).C++ - eccezioni e gestione delle risorse (RAII) →). Con l'ordine inverso (prima delete[] e poi new) un fallimento lascerebbe data_ dangling. Chiamare convert più volte sullo stesso oggetto è lecito: il vecchio contenuto viene rilasciato, senza leak.
Un static_assert rende chiaro l'errore se si passa un contenitore di elementi non convertibili in int: con una std::map<int, std::string> il compilatore dice static assertion failed ... il contenitore deve contenere elementi convertibili in int, invece di un errore criptico dentro std::copy.
2. Il codice
#ifndef CONVERT_TO_ARRAY_H
#define CONVERT_TO_ARRAY_H
#include <algorithm>
#include <cstddef>
#include <iterator>
#include <type_traits>
// Converte il contenuto di un qualsiasi contenitore STL di int in un array di int allocato dinamicamente.
// Invarianti: data_ punta a un buffer di size_ int (nullptr se size_ == 0); la classe ne e' proprietaria.
class ConvertToArray {
public:
ConvertToArray(); // vuoto
ConvertToArray(const ConvertToArray& other); // copia profonda
ConvertToArray& operator=(const ConvertToArray& other);
ConvertToArray(ConvertToArray&& other) noexcept; // spostamento
ConvertToArray& operator=(ConvertToArray&& other) noexcept;
~ConvertToArray();
// Accetta QUALSIASI contenitore STL: basta che abbia begin() e end() e che contenga elementi
// convertibili in int (vector, list, deque, set, array, forward_list, ...).
template <typename Container>
void convert(const Container& c)
{
static_assert(std::is_convertible<typename Container::value_type, int>::value,
"il contenitore deve contenere elementi convertibili in int");
std::size_t n = static_cast<std::size_t>(std::distance(c.begin(), c.end())); // size() non c'e' in forward_list
int* fresh = new int[n]; // prima si alloca: se fallisce, *this e' intatto
std::copy(c.begin(), c.end(), fresh);
delete[] data_; // solo ora si libera il vecchio buffer
data_ = fresh;
size_ = n;
}
std::size_t size() const { return size_; }
const int* data() const { return data_; }
int operator[](std::size_t i) const { return data_[i]; } // senza controllo, come l'array
private:
void swap(ConvertToArray& other) noexcept;
int* data_;
std::size_t size_;
};
#endif // CONVERT_TO_ARRAY_H#include "convert_to_array.h"
#include <utility>
ConvertToArray::ConvertToArray() : data_{nullptr}, size_{0} {}
ConvertToArray::ConvertToArray(const ConvertToArray& other)
: data_{other.size_ ? new int[other.size_] : nullptr}, size_{other.size_}
{
std::copy(other.data_, other.data_ + other.size_, data_);
}
ConvertToArray& ConvertToArray::operator=(const ConvertToArray& other)
{
ConvertToArray copy{other}; // se la copia fallisce *this non cambia
swap(copy); // e il vecchio buffer lo libera il distruttore di copy
return *this;
}
ConvertToArray::ConvertToArray(ConvertToArray&& other) noexcept : data_{nullptr}, size_{0}
{
swap(other); // other resta vuoto e valido
}
ConvertToArray& ConvertToArray::operator=(ConvertToArray&& other) noexcept
{
swap(other);
return *this;
}
ConvertToArray::~ConvertToArray() { delete[] data_; }
void ConvertToArray::swap(ConvertToArray& other) noexcept
{
std::swap(data_, other.data_);
std::swap(size_, other.size_);
}Le operazioni essenziali:
- costruttore di default: buffer assente (
nullptr),size_ = 0(delete[] nullptrè lecito); - copia: alloca un buffer nuovo di
other.size_elementi e copia;new int[0]sarebbe lecito ma si evita connullptrper le dimensioni nulle; - assegnamento di copia: copy-and-swap: si fa una copia locale (può lanciare, ma
*thisè intatto), poi si scambiano i membri conswap(noexcept) e il distruttore della copia libera il vecchio buffer; è corretto anche pera = a; - spostamento: costruttore e assegnamento scambiano con l'altro oggetto: la sorgente resta in uno stato valido (vuota) e viene distrutta senza danni;
- distruttore:
delete[](con le parentesi quadre, perché si è fattonew[]).
#include <array>
#include <deque>
#include <forward_list>
#include <iostream>
#include <list>
#include <set>
#include <utility>
#include <vector>
#include "convert_to_array.h"
namespace {
void print(const char* name, const ConvertToArray& a)
{
std::cout << name << " (" << a.size() << "):";
for (std::size_t i = 0; i < a.size(); ++i) std::cout << ' ' << a[i];
std::cout << '\n';
}
}
int main()
{
ConvertToArray conv;
print("vuoto", conv);
conv.convert(std::vector<int>{3, 1, 4, 1, 5});
print("vector", conv);
conv.convert(std::list<int>{9, 8, 7}); // stesso oggetto, un altro contenitore
print("list", conv);
conv.convert(std::deque<int>{10, 20});
print("deque", conv);
conv.convert(std::set<int>{5, 3, 5, 1}); // il set elimina i duplicati e ordina
print("set", conv);
conv.convert(std::array<int, 4>{1, 2, 3, 4});
print("array", conv);
conv.convert(std::forward_list<int>{6, 6, 6}); // niente size(): distance funziona lo stesso
print("forward_list", conv);
conv.convert(std::vector<int>{}); // contenitore vuoto
print("vector vuoto", conv);
// la copia e' profonda: modificare il contenitore o l'originale non tocca la copia
conv.convert(std::vector<int>{1, 2, 3});
ConvertToArray copy = conv;
conv.convert(std::vector<int>{7});
print("originale", conv);
print("copia", copy);
ConvertToArray moved = std::move(copy); // spostamento: copy resta vuoto e valido
print("spostata", moved);
print("sorgente dopo move", copy);
}Uscita eseguita (compilato anche per Linux con -Wall -Wextra senza avvisi):
vuoto (0):
vector (5): 3 1 4 1 5
list (3): 9 8 7
deque (2): 10 20
set (3): 1 3 5
array (4): 1 2 3 4
forward_list (3): 6 6 6
vector vuoto (0):
originale (1): 7
copia (3): 1 2 3
spostata (3): 1 2 3
sorgente dopo move (0):Che cosa dimostrano le righe: lo stesso oggetto conv converte contenitori diversi uno dopo l'altro (il vecchio buffer viene sostituito ogni volta); il set elimina i duplicati e ordina, quindi {5, 3, 5, 1} diventa 1 3 5; un contenitore vuoto produce un array di 0 elementi; dopo la copia, riassegnare l'originale non tocca la copia (copia profonda: due buffer distinti); dopo lo spostamento la sorgente è vuota.
3. Errori da evitare
convert(const std::vector<int>&): non gestisce "qualsiasi container".- Definire il template in un
.cpp: il compilatore non vede il corpo dove lo istanzia (errore di linking). - Usare
c.size(): non esiste perforward_list. - Dimenticare la copia profonda: due oggetti con lo stesso puntatore fanno due
delete[](doppia deallocazione). delete data_senza[].delete[]del vecchio buffer prima di aver allocato il nuovo.- Restituire dalla funzione un puntatore al buffer interno e lasciare che qualcuno faccia
delete.
Versione ripasso
- Testo: classe
ConvertToArraycon costruttore di default,convertche accetta qualsiasi container STL diint, alloca un buffer adeguato e vi copia gli elementi, più ogni altra funzione per la buona gestione della memoria;maindi test; indicare la divisione in file. 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 , 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_listtipo 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 → 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 → . - Scelte:
template <typename Container> void convert(const Container&)nell'header (polimorfismo statico); dimensione constd::distance(begin, end)(ancheforward_list); copia constd::copy;static_assertsulvalue_type. File:convert_to_array.h,convert_to_array.cpp,main.cpp. - Ordine in
convert: alloca il nuovo buffer, copia, poidelete[]del vecchio (garanzia forte). - Operazioni essenziali: costruttore di default (
nullptr, 0), copia profonda, assegnamento con copy-and-swap, spostamento conswap, distruttore condelete[].
#ifndef CONVERT_TO_ARRAY_H
#define CONVERT_TO_ARRAY_H
#include <algorithm>
#include <cstddef>
#include <iterator>
#include <type_traits>
// Converte il contenuto di un qualsiasi contenitore STL di int in un array di int allocato dinamicamente.
// Invarianti: data_ punta a un buffer di size_ int (nullptr se size_ == 0); la classe ne e' proprietaria.
class ConvertToArray {
public:
ConvertToArray(); // vuoto
ConvertToArray(const ConvertToArray& other); // copia profonda
ConvertToArray& operator=(const ConvertToArray& other);
ConvertToArray(ConvertToArray&& other) noexcept; // spostamento
ConvertToArray& operator=(ConvertToArray&& other) noexcept;
~ConvertToArray();
// Accetta QUALSIASI contenitore STL: basta che abbia begin() e end() e che contenga elementi
// convertibili in int (vector, list, deque, set, array, forward_list, ...).
template <typename Container>
void convert(const Container& c)
{
static_assert(std::is_convertible<typename Container::value_type, int>::value,
"il contenitore deve contenere elementi convertibili in int");
std::size_t n = static_cast<std::size_t>(std::distance(c.begin(), c.end())); // size() non c'e' in forward_list
int* fresh = new int[n]; // prima si alloca: se fallisce, *this e' intatto
std::copy(c.begin(), c.end(), fresh);
delete[] data_; // solo ora si libera il vecchio buffer
data_ = fresh;
size_ = n;
}
std::size_t size() const { return size_; }
const int* data() const { return data_; }
int operator[](std::size_t i) const { return data_[i]; } // senza controllo, come l'array
private:
void swap(ConvertToArray& other) noexcept;
int* data_;
std::size_t size_;
};
#endif // CONVERT_TO_ARRAY_H- Uscita:
vector (5): 3 1 4 1 5,list (3): 9 8 7,deque (2): 10 20,set (3): 1 3 5,array (4): 1 2 3 4,forward_list (3): 6 6 6; la copia resta1 2 3quando l'originale diventa7; la sorgente dopo il move è vuota. - Errori tipici: parametro solo
vector<int>, template in un.cpp,c.size(), copia superficiale,deletesenza[],delete[]prima dellanew.