Salta al contenuto
Note per Studenti Esercizio - ConvertToArray da un container STL (secondo appello 2023-24, programmazione)

Esercizio - ConvertToArray da un container STL (secondo appello 2023-24, programmazione)

Esame
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 (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 →, STL - sequenze, iteratori e container (vector, list, string)La STL (Standard Template Library) fornisce contenitori (vector, list, map, ...) e algoritmi generici (sort, find, accumulate, ...) tenuti insieme da una interfaccia unificata: la sequenza, un intervallo [begin, end) di elementi gestito tramite iteratori (oggetti che identificano un elemento: * dereferenzia, ++ avanza, == e != confrontano; end è il primo elemento dopo l'ultimo, quindi una sequenza vuota ha begin == end). Gli algoritmi conoscono solo gli iteratori e i contenitori espongono solo iteratori, così i due mondi sono disaccoppiati e cambiare contenitore non cambia l'algoritmo. Un iteratore di lista è una classe annidata con un puntatore al nodo; auto deduce il tipo dell'iteratore. vector (contiguo, [] e at(), insert/erase costosi e che invalidano gli iteratori), string (un vector di char con + e +=), list (nodi concatenati, insert/erase senza spostare gli altri elementi, niente []) e char[] (array C: dimensione fissa, niente iteratori, == confronta i puntatori) a confronto.STL - sequenze, iteratori e container (vector, list, string) →.


1. Lettura del testo

Tre parole del testo decidono il progetto.

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

  1. si calcola n;
  2. si alloca il buffer nuovo (new int[n]) prima di toccare quello vecchio;
  3. si copia con std::copy;
  4. solo ora delete[] del vecchio buffer, e si aggiornano data_ e size_.

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

toarray/include/convert_to_array.hcpp
#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
toarray/src/convert_to_array.cppcpp
#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 con nullptr per le dimensioni nulle;
  • assegnamento di copia: copy-and-swap: si fa una copia locale (può lanciare, ma *this è intatto), poi si scambiano i membri con swap (noexcept) e il distruttore della copia libera il vecchio buffer; è corretto anche per a = 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 è fatto new[]).
toarray/src/main.cppcpp
#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 per forward_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

toarray/include/convert_to_array.hcpp
#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 resta 1 2 3 quando l'originale diventa 7; la sorgente dopo il move è vuota.
  • Errori tipici: parametro solo vector<int>, template in un .cpp, c.size(), copia superficiale, delete senza [], delete[] prima della new.

Esercizi su questo argomento

Teoria collegata