Salta al contenuto
Note per Studenti Esercizio - InertialDriver con buffer circolare (prova intermedia 2025-26)

Esercizio - InertialDriver con buffer circolare (prova intermedia 2025-26)

Esame
In questa pagina 5

Testo. Sviluppare un modulo C++ per i dati di un sistema di sensori inerziali per il tracciamento della posa del corpo umano. Il sistema ha 17 sensori; ciascuno fornisce una lettura di 6 valori double, chiamati yaw_v, yaw_a, pitch_v, pitch_a, roll_v, roll_a (velocità e accelerazione delle rotazioni attorno ai tre assi), da rappresentare con una struttura dati opportuna. Il sistema nel suo complesso fornisce una misura, composta da 17 letture.

La classe InertialDriver riceve le misure dal sensore (il produttore) e le fornisce a richiesta all'utilizzatore (il consumatore), nello stesso ordine di arrivo. La misura è organizzata obbligatoriamente come array stile C di 17 elementi. Le misure in arrivo sono tenute in un buffer gestito obbligatoriamente con la classe MyVector sviluppata in laboratorio (con o senza template). Il buffer contiene al massimo BUFFER_DIM misure (costante definita in una posizione opportuna) con politica circolare: quando è pieno, l'arrivo di una nuova misura sovrascrive la meno recente.

Funzioni di InertialDriver, da realizzare con esattamente questi nomi e completando tipi di ritorno, reference e const:

  • push_back: accetta un array stile C con una misura e la memorizza (sovrascrivendo la meno recente se il buffer è pieno);
  • pop_front: restituisce in output un array stile C con la misura più vecchia e la rimuove dal buffer;
  • clear_buffer: elimina, senza restituirle, tutte le misure salvate;
  • get_reading: accetta un numero tra 0 e 16 e ritorna la lettura corrispondente della misura più recente, senza cancellarla;
  • operator<<: stampa a schermo l'ultima misura salvata (senza rimuoverla).

Organizzazione: classi divise in .h e .cpp; un altro .cpp con il main di test; cartelle include/ e src/ come nel progetto Rational; un README.txt con le note per chi corregge; compilabile sulla macchina virtuale; consegna senza eseguibile. Teoria: 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 →, 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 →.


1. Dal testo alla struttura

Il testo contiene molte scelte già prese (nomi, array C, MyVector). Conviene ricavare una tabella dei requisiti prima di scrivere codice:

requisito scelta
lettura di 6 double con nomi fissati struct Reading con 6 membri; una struct perché ogni valore è ammissibile (nessun invariante)
misura = 17 letture, array C un array Reading[17]; poiché MyVector<T> deve copiare i T e un array nudo non è assegnabile, lo si avvolge in struct Measure { Reading reading[N_SENSORS]; } (una struct con membro array si copia membro a membro, quindi anche l'array)
buffer di BUFFER_DIM misure con MyVector MyVector<Measure> buffer_ creato con BUFFER_DIM elementi, usato come anello
costante BUFFER_DIM, 17 constexpr nell'header di InertialDriver, accanto alle altre costanti: nessun magic number nel codice
politica circolare due indici: head_ (posizione della misura più vecchia) e count_ (misure presenti)
push_back / pop_front con array C parametri riferimento a array: const Reading (&)[N_SENSORS] in ingresso, Reading (&)[N_SENSORS] in uscita
get_reading della più recente restituisce const Reading&; fuori da 0..16 o buffer vuoto lancia out_of_range
operator<< funzione libera che usa solo l'interfaccia pubblica, non serve friend

Perché un riferimento a array

In C++ un parametro scritto const Reading m[17] è in realtà un const Reading*: la lunghezza si perde (è la stessa trappola del lab 3, esercizio 2). Scrivendo const Reading (&m)[17] il tipo contiene la dimensione, e il compilatore rifiuta array di lunghezza diversa. Il chiamante scrive comunque driver.push_back(m) con m dichiarato Reading m[N_SENSORS]. Il const in ingresso dice che la funzione non modifica la misura; in uscita il riferimento non è const perché la funzione scrive nell'array del chiamante.

Il buffer circolare con head_ e count_

MyVector<Measure> ha sempre BUFFER_DIM posizioni fisiche (0..BUFFER_DIM-1). La k-esima misura (0 = la più vecchia) si trova nella posizione (head_ + k) % BUFFER_DIM. Invarianti: 0 <= count_ <= BUFFER_DIM, head_ < BUFFER_DIM.

  • push_back: la nuova misura va dopo l'ultima, cioè in index(count_). Se il buffer non è pieno count_ aumenta. Se è pieno index(count_) coincide con head_: si sovrascrive la più vecchia e head_ avanza di una posizione, perché la nuova più vecchia è la successiva.
  • pop_front: copia la misura in head_ in out, poi head_ avanza e count_ diminuisce.
  • clear_buffer: basta head_ = 0; count_ = 0; — i dati vecchi restano nelle posizioni fisiche ma non sono più raggiungibili, e verranno sovrascritti.
  • get_reading(i): la misura più recente è la (count_-1)-esima: buffer_[index(count_ - 1)].reading[i].

Traccia per BUFFER_DIM = 5, con misure numerate 1..7 (le posizioni fisiche contengono il numero della misura; - = mai scritta):

operazione posizioni 0..4 head_ count_ misura più vecchia
inizio - - - - - 0 0
push 1, 2, 3 1 2 3 - - 0 3 1
push 4, 5 1 2 3 4 5 0 5 1
push 6 (pieno: sovrascrive la 1) 6 2 3 4 5 1 5 2
push 7 (sovrascrive la 2) 6 7 3 4 5 2 5 3
pop_front (restituisce la 3) 6 7 3 4 5 3 4 4
pop_front (restituisce la 4) 6 7 3 4 5 4 3 5
push 8 (posizione (4+3)%5 = 2) 6 7 8 4 5 4 4 5

Nel test del main si fa esattamente questa sequenza (7 push_back, due pop_front, un altro push_back).

2. MyVector (template, definizioni fuori dalla classe)

MyVector<T> è la classe del laboratorio 5: buffer dinamico più grande del numero di elementi, con size_ e capacity_; la riallocazione raddoppia la capacità, quindi push_back costa in media una quantità costante; le funzioni membro sono definite fuori dalla classe (nel file .hpp), dopo la sezione public e prima della private, come richiesto dal laboratorio.

inertial/include/myvector.hcpp
#ifndef MYVECTOR_H
#define MYVECTOR_H

#include <cstddef>

// Vettore dinamico di T, in stile std::vector (versione con template del laboratorio 5).
// Invarianti: size_ <= capacity_; data_ punta a un buffer di capacity_ elementi (nullptr se capacity_ == 0).
template <typename T>
class MyVector {
public:
    MyVector();                                   // vettore vuoto
    explicit MyVector(std::size_t n);             // n elementi inizializzati con T{}
    MyVector(const MyVector& other);              // copia profonda
    MyVector& operator=(const MyVector& other);
    MyVector(MyVector&& other) noexcept;          // spostamento
    MyVector& operator=(MyVector&& other) noexcept;
    ~MyVector();

    T& operator[](std::size_t i);                 // nessun controllo
    const T& operator[](std::size_t i) const;
    T& at(std::size_t i);                         // controllo dei limiti: lancia out_of_range
    const T& at(std::size_t i) const;

    void push_back(const T& value);
    void pop_back();
    void reserve(std::size_t n);                  // capacita' >= n; non fa nulla se gia' sufficiente
    void clear();                                 // size = 0, la capacita' resta

    std::size_t size() const;
    std::size_t capacity() const;

private:
    void swap(MyVector& other) noexcept;
    std::size_t size_;
    std::size_t capacity_;
    T* data_;
};

#include "myvector.hpp"                           // le definizioni dei template stanno in src/

#endif // MYVECTOR_H
inertial/src/myvector.hppcpp
#ifndef MYVECTOR_HPP
#define MYVECTOR_HPP

#include <algorithm>
#include <stdexcept>
#include <utility>

template <typename T>
MyVector<T>::MyVector() : size_{0}, capacity_{0}, data_{nullptr} {}

template <typename T>
MyVector<T>::MyVector(std::size_t n) : size_{n}, capacity_{n}, data_{n ? new T[n]{} : nullptr} {}

template <typename T>
MyVector<T>::MyVector(const MyVector& other)
    : size_{other.size_}, capacity_{other.size_}, data_{other.size_ ? new T[other.size_] : nullptr}
{
    std::copy(other.data_, other.data_ + other.size_, data_);
}

template <typename T>
MyVector<T>& MyVector<T>::operator=(const MyVector& other)
{
    MyVector copy{other};          // se la copia fallisce, *this e' intatto
    swap(copy);                    // il vecchio buffer viene liberato dal distruttore di copy
    return *this;
}

template <typename T>
MyVector<T>::MyVector(MyVector&& other) noexcept : size_{0}, capacity_{0}, data_{nullptr}
{
    swap(other);                   // other resta vuoto e valido
}

template <typename T>
MyVector<T>& MyVector<T>::operator=(MyVector&& other) noexcept
{
    swap(other);                   // other riceve il vecchio buffer e lo libera alla distruzione
    return *this;
}

template <typename T>
MyVector<T>::~MyVector() { delete[] data_; }

template <typename T>
T& MyVector<T>::operator[](std::size_t i) { return data_[i]; }

template <typename T>
const T& MyVector<T>::operator[](std::size_t i) const { return data_[i]; }

template <typename T>
T& MyVector<T>::at(std::size_t i)
{
    if (i >= size_) throw std::out_of_range("MyVector::at: indice fuori dai limiti");
    return data_[i];
}

template <typename T>
const T& MyVector<T>::at(std::size_t i) const
{
    if (i >= size_) throw std::out_of_range("MyVector::at: indice fuori dai limiti");
    return data_[i];
}

template <typename T>
void MyVector<T>::push_back(const T& value)
{
    if (size_ == capacity_) reserve(capacity_ ? 2 * capacity_ : 1);   // raddoppio: push_back ammortizzato O(1)
    data_[size_++] = value;
}

template <typename T>
void MyVector<T>::pop_back()
{
    if (size_ == 0) throw std::out_of_range("MyVector::pop_back: vettore vuoto");
    --size_;
}

template <typename T>
void MyVector<T>::reserve(std::size_t n)
{
    if (n <= capacity_) return;
    T* fresh = new T[n]{};         // prima si alloca: se new fallisce il vettore resta com'era
    std::copy(data_, data_ + size_, fresh);
    delete[] data_;
    data_ = fresh;
    capacity_ = n;
}

template <typename T>
void MyVector<T>::clear() { size_ = 0; }

template <typename T>
std::size_t MyVector<T>::size() const { return size_; }

template <typename T>
std::size_t MyVector<T>::capacity() const { return capacity_; }

template <typename T>
void MyVector<T>::swap(MyVector& other) noexcept
{
    std::swap(size_, other.size_);
    std::swap(capacity_, other.capacity_);
    std::swap(data_, other.data_);
}

#endif // MYVECTOR_HPP

Punti da notare:

3. InertialDriver

inertial/include/inertial_driver.hcpp
#ifndef INERTIAL_DRIVER_H
#define INERTIAL_DRIVER_H

#include <cstddef>
#include <iosfwd>
#include "myvector.h"

constexpr int N_SENSORS = 17;                    // sensori del sistema: letture in una misura
constexpr std::size_t BUFFER_DIM = 5;            // misure contenute nel buffer circolare

// Lettura di un sensore: velocita' e accelerazione delle rotazioni attorno ai tre assi.
struct Reading {
    double yaw_v, yaw_a;
    double pitch_v, pitch_a;
    double roll_v, roll_a;
};

// Misura = 17 letture. Il requisito impone un array stile C: lo si avvolge in una struct
// solo perche' MyVector<T> deve poter copiare T (un array nudo non e' assegnabile).
struct Measure {
    Reading reading[N_SENSORS];
};

class InertialDriver {
public:
    InertialDriver();                                         // buffer vuoto

    // aggiunge una misura; se il buffer e' pieno sovrascrive la meno recente
    void push_back(const Reading (&measure)[N_SENSORS]);
    // copia in 'out' la misura piu' vecchia e la rimuove (out_of_range se il buffer e' vuoto)
    void pop_front(Reading (&out)[N_SENSORS]);
    void clear_buffer();                                      // elimina tutte le misure

    // lettura 'sensor' (0..16) della misura piu' recente, senza rimuoverla
    // (out_of_range se sensor e' fuori da 0..16 o se il buffer e' vuoto)
    const Reading& get_reading(int sensor) const;

    std::size_t size() const;                                 // misure presenti
    bool empty() const;

private:
    std::size_t index(std::size_t k) const;                   // posizione fisica della k-esima misura (0 = piu' vecchia)

    MyVector<Measure> buffer_;                                // BUFFER_DIM posizioni, usate in modo circolare
    std::size_t head_;                                        // posizione della misura piu' vecchia
    std::size_t count_;                                       // misure presenti (0..BUFFER_DIM)
};

// stampa l'ultima misura salvata (senza rimuoverla); se il buffer e' vuoto stampa un avviso
std::ostream& operator<<(std::ostream& os, const InertialDriver& driver);

#endif // INERTIAL_DRIVER_H
inertial/src/inertial_driver.cppcpp
#include "inertial_driver.h"
#include <algorithm>
#include <ostream>
#include <stdexcept>

InertialDriver::InertialDriver() : buffer_(BUFFER_DIM), head_{0}, count_{0} {}

std::size_t InertialDriver::index(std::size_t k) const
{
    return (head_ + k) % BUFFER_DIM;
}

void InertialDriver::push_back(const Reading (&measure)[N_SENSORS])
{
    // posizione di scrittura: subito dopo l'ultima misura; se il buffer e' pieno coincide con head_,
    // cioe' con la misura piu' vecchia, che viene sovrascritta
    std::size_t pos = index(count_);
    std::copy(measure, measure + N_SENSORS, buffer_[pos].reading);
    if (count_ < BUFFER_DIM) {
        ++count_;
    } else {
        head_ = (head_ + 1) % BUFFER_DIM;       // la piu' vecchia ora e' la successiva
    }
}

void InertialDriver::pop_front(Reading (&out)[N_SENSORS])
{
    if (count_ == 0) throw std::out_of_range("InertialDriver::pop_front: buffer vuoto");
    const Measure& oldest = buffer_[head_];
    std::copy(oldest.reading, oldest.reading + N_SENSORS, out);
    head_ = (head_ + 1) % BUFFER_DIM;
    --count_;
}

void InertialDriver::clear_buffer()
{
    head_ = 0;
    count_ = 0;
}

const Reading& InertialDriver::get_reading(int sensor) const
{
    if (count_ == 0) throw std::out_of_range("InertialDriver::get_reading: buffer vuoto");
    if (sensor < 0 || sensor >= N_SENSORS) throw std::out_of_range("InertialDriver::get_reading: sensore fuori da 0..16");
    return buffer_[index(count_ - 1)].reading[sensor];
}

std::size_t InertialDriver::size() const { return count_; }
bool InertialDriver::empty() const { return count_ == 0; }

std::ostream& operator<<(std::ostream& os, const InertialDriver& driver)
{
    if (driver.empty()) return os << "(buffer vuoto)\n";
    for (int i = 0; i < N_SENSORS; ++i) {
        const Reading& r = driver.get_reading(i);
        os << "sensore " << i << ": yaw " << r.yaw_v << '/' << r.yaw_a
           << ", pitch " << r.pitch_v << '/' << r.pitch_a
           << ", roll " << r.roll_v << '/' << r.roll_a << '\n';
    }
    return os;
}

Scelte da giustificare al correttore:

  • l'ordine di inizializzazione dei membri segue l'ordine di dichiarazione (buffer_, head_, count_);
  • buffer_(BUFFER_DIM) con le parentesi tonde: con le graffe buffer_{BUFFER_DIM} si sceglierebbe un eventuale costruttore a initializer_list; qui non esiste ma le tonde evitano ambiguità;
  • std::copy copia i 17 elementi tra array; una for esplicita andrebbe ugualmente bene;
  • index(k) è l'unico punto con l'aritmetica modulare: una sola funzione privata, nessun % ripetuto;
  • size() e empty() non erano richieste ma sono "ogni altra funzione ritenuta necessaria" (servono al main per i test);
  • get_reading restituisce un const Reading&, senza copiare, ed è const perché non modifica il driver.

4. Il main di test e l'uscita

inertial/src/main.cppcpp
#include <iostream>
#include "inertial_driver.h"

// Misura numero k: la lettura del sensore i ha yaw_v = 100*k + i, gli altri campi derivati.
static void make_measure(int k, Reading (&m)[N_SENSORS])
{
    for (int i = 0; i < N_SENSORS; ++i) {
        double base = 100.0 * k + i;
        m[i] = Reading{base, base + 0.1, base + 0.2, base + 0.3, base + 0.4, base + 0.5};
    }
}

static void check(bool ok, const char* what)
{
    std::cout << (ok ? "OK    " : "ERRORE ") << what << '\n';
}

int main()
{
    InertialDriver driver;
    check(driver.empty() && driver.size() == 0, "buffer inizialmente vuoto");

    Reading m[N_SENSORS];
    for (int k = 1; k <= 3; ++k) {                  // 3 misure: il buffer (5 posti) non e' pieno
        make_measure(k, m);
        driver.push_back(m);
    }
    check(driver.size() == 3, "dopo 3 push_back size == 3");
    check(driver.get_reading(0).yaw_v == 300.0, "get_reading(0) e' dell'ultima misura (k=3)");
    check(driver.get_reading(16).roll_a == 316.5, "get_reading(16).roll_a == 316.5");

    for (int k = 4; k <= 7; ++k) {                  // altre 4: 7 misure in un buffer da 5, due sovrascritte
        make_measure(k, m);
        driver.push_back(m);
    }
    check(driver.size() == BUFFER_DIM, "buffer pieno: size == BUFFER_DIM");

    Reading out[N_SENSORS];
    driver.pop_front(out);                          // la piu' vecchia rimasta e' la numero 3
    check(out[0].yaw_v == 300.0 && out[16].yaw_v == 316.0, "pop_front restituisce la misura piu' vecchia (k=3)");
    check(driver.size() == BUFFER_DIM - 1, "dopo pop_front size == BUFFER_DIM - 1");
    driver.pop_front(out);
    check(out[0].yaw_v == 400.0, "il pop_front successivo restituisce k=4");

    make_measure(8, m);                             // si rimette in coda dopo due estrazioni
    driver.push_back(m);
    check(driver.size() == BUFFER_DIM - 1, "dopo un push_back size == BUFFER_DIM - 1");
    check(driver.get_reading(1).yaw_v == 801.0, "la piu' recente e' k=8");

    std::cout << "--- ultima misura (operator<<) ---\n" << driver;

    driver.clear_buffer();
    check(driver.empty(), "clear_buffer svuota il buffer");
    std::cout << driver;                            // stampa dell'avviso sul buffer vuoto
    return 0;
}

Compilato in WebAssembly (con la throw sostituita da abort solo per la prova, perché le eccezioni non funzionano in quell'ambiente; il percorso normale non lancia mai) il programma stampa:

OK    buffer inizialmente vuoto
OK    dopo 3 push_back size == 3
OK    get_reading(0) e' dell'ultima misura (k=3)
OK    get_reading(16).roll_a == 316.5
OK    buffer pieno: size == BUFFER_DIM
OK    pop_front restituisce la misura piu' vecchia (k=3)
OK    dopo pop_front size == BUFFER_DIM - 1
OK    il pop_front successivo restituisce k=4
OK    dopo un push_back size == BUFFER_DIM - 1
OK    la piu' recente e' k=8
--- ultima misura (operator<<) ---
sensore 0: yaw 800/800.1, pitch 800.2/800.3, roll 800.4/800.5
sensore 1: yaw 801/801.1, pitch 801.2/801.3, roll 801.4/801.5
...
sensore 16: yaw 816/816.1, pitch 816.2/816.3, roll 816.4/816.5
OK    clear_buffer svuota il buffer
(buffer vuoto)

(Le righe dei sensori da 2 a 15 seguono lo stesso schema e sono omesse.) La misura numero k ha yaw_v = 100*k + i per il sensore i: leggere 300 nel primo pop_front dimostra che la più vecchia rimasta dopo 7 push in un buffer da 5 è la numero 3, come nella tabella.

I casi di errore

I percorsi con eccezione si provano a parte, con try/catch (programma compilato per Linux senza avvisi con -Wall -Wextra, non eseguito in WebAssembly):

inertial/src/test_errori.cppcpp
#include <iostream>
#include <stdexcept>
#include "inertial_driver.h"

int main()
{
    InertialDriver driver;
    Reading out[N_SENSORS];

    try { driver.pop_front(out); }                 // buffer vuoto
    catch (const std::out_of_range& e) { std::cout << "pop_front su vuoto: " << e.what() << '\n'; }

    try { driver.get_reading(0); }                 // buffer vuoto
    catch (const std::out_of_range& e) { std::cout << "get_reading su vuoto: " << e.what() << '\n'; }

    Reading m[N_SENSORS] = {};
    driver.push_back(m);
    try { driver.get_reading(17); }                // indice fuori da 0..16
    catch (const std::out_of_range& e) { std::cout << "get_reading(17): " << e.what() << '\n'; }
    try { driver.get_reading(-1); }
    catch (const std::out_of_range& e) { std::cout << "get_reading(-1): " << e.what() << '\n'; }

    MyVector<int> v;
    v.push_back(1);
    try { v.at(1) = 5; }                           // size == 1: l'indice 1 e' fuori
    catch (const std::out_of_range& e) { std::cout << "MyVector::at(1): " << e.what() << '\n'; }
}

L'uscita prevista, ricavata dal codice, è:

pop_front su vuoto: InertialDriver::pop_front: buffer vuoto
get_reading su vuoto: InertialDriver::get_reading: buffer vuoto
get_reading(17): InertialDriver::get_reading: sensore fuori da 0..16
get_reading(-1): InertialDriver::get_reading: sensore fuori da 0..16
MyVector::at(1): MyVector::at: indice fuori dai limiti

5. Cosa controlla il correttore

Dalla griglia del corso (regole del progetto finale, che valgono anche qui) e dal testo:

  • struttura e file: include/*.h, src/*.cpp, un solo main, README.txt con le note per chi corregge (attività di ciascun membro, problemi non risolti), nessun eseguibile nell'archivio; il progetto compila e si ricontrolla dopo la consegna scaricandolo in un'altra cartella;
  • memoria: nessun leak (il buffer è posseduto da MyVector, che libera con delete[]), nessun accesso fuori dai limiti (index calcola sempre modulo BUFFER_DIM);
  • nomi esatti delle funzioni (push_back, pop_front, clear_buffer, get_reading) e const corretti;
  • magic number: 17 e 5 solo nelle costanti N_SENSORS e BUFFER_DIM;
  • stile: funzioni brevi, commenti utili, un .h e un .cpp per classe, include guard;
  • niente friend, niente variabili globali dove servono membri privati.

La stessa struttura viene poi compilata con CMake nell'esercizio Esercizio - Ristrutturare un progetto con CMake (laboratorio 9).

Versione ripasso

  • Testo: InertialDriver riceve misure da 17 sensori (lettura = 6 double: yaw_v, yaw_a, pitch_v, pitch_a, roll_v, roll_a; misura = array C di 17 letture) in un buffer circolare di BUFFER_DIM misure gestito con MyVector. Funzioni con nomi esatti: push_back, pop_front, clear_buffer, get_reading(0..16) della più recente, operator<< (ultima misura). Cartelle include/ e src/, README.txt, main in file a parte, nessun eseguibile.
  • Scelte: struct Reading (nessun invariante); struct Measure { Reading reading[N_SENSORS]; } perché MyVector<T> deve copiare i T e un array nudo non è assegnabile; costanti N_SENSORS = 17 e BUFFER_DIM con constexpr; parametri riferimento a array (const Reading (&)[N_SENSORS]), così la dimensione è nel tipo.
  • Anello: MyVector<Measure> buffer_(BUFFER_DIM), indici head_ (più vecchia) e count_; la k-esima misura è in (head_ + k) % BUFFER_DIM. push_back scrive in index(count_): se pieno coincide con head_ (sovrascrive la più vecchia) e head_ avanza. pop_front copia da head_, poi avanza head_ e fa --count_. clear_buffer: head_ = count_ = 0. get_reading(i): buffer_[index(count_ - 1)].reading[i].
  • Traccia (dim 5): push 1..5 → 1 2 3 4 5 (head 0); push 6 → 6 2 3 4 5 (head 1); push 7 → 6 7 3 4 5 (head 2, più vecchia = 3); due pop_front restituiscono 3 e 4 (head 4, count 3); push 8 in posizione 2 → 6 7 8 4 5.
  • MyVector<T>: size_, capacity_, data_; operazioni essenziali; assegnamento con copy-and-swap; reserve(n) non fa nulla se la capacità basta, altrimenti alloca prima e poi copia; push_back raddoppia la capacità; operator[] senza controllo, at() con out_of_range.
  • Nucleo del codice:
inertial/src/inertial_driver.cppcpp
#include "inertial_driver.h"
#include <algorithm>
#include <ostream>
#include <stdexcept>

InertialDriver::InertialDriver() : buffer_(BUFFER_DIM), head_{0}, count_{0} {}

std::size_t InertialDriver::index(std::size_t k) const
{
    return (head_ + k) % BUFFER_DIM;
}

void InertialDriver::push_back(const Reading (&measure)[N_SENSORS])
{
    // posizione di scrittura: subito dopo l'ultima misura; se il buffer e' pieno coincide con head_,
    // cioe' con la misura piu' vecchia, che viene sovrascritta
    std::size_t pos = index(count_);
    std::copy(measure, measure + N_SENSORS, buffer_[pos].reading);
    if (count_ < BUFFER_DIM) {
        ++count_;
    } else {
        head_ = (head_ + 1) % BUFFER_DIM;       // la piu' vecchia ora e' la successiva
    }
}

void InertialDriver::pop_front(Reading (&out)[N_SENSORS])
{
    if (count_ == 0) throw std::out_of_range("InertialDriver::pop_front: buffer vuoto");
    const Measure& oldest = buffer_[head_];
    std::copy(oldest.reading, oldest.reading + N_SENSORS, out);
    head_ = (head_ + 1) % BUFFER_DIM;
    --count_;
}

void InertialDriver::clear_buffer()
{
    head_ = 0;
    count_ = 0;
}

const Reading& InertialDriver::get_reading(int sensor) const
{
    if (count_ == 0) throw std::out_of_range("InertialDriver::get_reading: buffer vuoto");
    if (sensor < 0 || sensor >= N_SENSORS) throw std::out_of_range("InertialDriver::get_reading: sensore fuori da 0..16");
    return buffer_[index(count_ - 1)].reading[sensor];
}

std::size_t InertialDriver::size() const { return count_; }
bool InertialDriver::empty() const { return count_ == 0; }

std::ostream& operator<<(std::ostream& os, const InertialDriver& driver)
{
    if (driver.empty()) return os << "(buffer vuoto)\n";
    for (int i = 0; i < N_SENSORS; ++i) {
        const Reading& r = driver.get_reading(i);
        os << "sensore " << i << ": yaw " << r.yaw_v << '/' << r.yaw_a
           << ", pitch " << r.pitch_v << '/' << r.pitch_a
           << ", roll " << r.roll_v << '/' << r.roll_a << '\n';
    }
    return os;
}
  • Correttore: struttura e file (include, src, README), nessun leak né accesso fuori limiti, nomi esatti, niente magic number, stile (funzioni brevi, include guard), niente friend né globali. Errori tipici: array come parametro (perde la dimensione), % ripetuti, sovrascrivere senza avanzare head_, get_reading senza controllo.

  • Codice (inertial/include/inertial_driver.h):

inertial/include/inertial_driver.hcpp
#ifndef INERTIAL_DRIVER_H
#define INERTIAL_DRIVER_H

#include <cstddef>
#include <iosfwd>
#include "myvector.h"

constexpr int N_SENSORS = 17;                    // sensori del sistema: letture in una misura
constexpr std::size_t BUFFER_DIM = 5;            // misure contenute nel buffer circolare

// Lettura di un sensore: velocita' e accelerazione delle rotazioni attorno ai tre assi.
struct Reading {
    double yaw_v, yaw_a;
    double pitch_v, pitch_a;
    double roll_v, roll_a;
};

// Misura = 17 letture. Il requisito impone un array stile C: lo si avvolge in una struct
// solo perche' MyVector<T> deve poter copiare T (un array nudo non e' assegnabile).
struct Measure {
    Reading reading[N_SENSORS];
};

class InertialDriver {
public:
    InertialDriver();                                         // buffer vuoto

    // aggiunge una misura; se il buffer e' pieno sovrascrive la meno recente
    void push_back(const Reading (&measure)[N_SENSORS]);
    // copia in 'out' la misura piu' vecchia e la rimuove (out_of_range se il buffer e' vuoto)
    void pop_front(Reading (&out)[N_SENSORS]);
    void clear_buffer();                                      // elimina tutte le misure

    // lettura 'sensor' (0..16) della misura piu' recente, senza rimuoverla
    // (out_of_range se sensor e' fuori da 0..16 o se il buffer e' vuoto)
    const Reading& get_reading(int sensor) const;

    std::size_t size() const;                                 // misure presenti
    bool empty() const;

private:
    std::size_t index(std::size_t k) const;                   // posizione fisica della k-esima misura (0 = piu' vecchia)

    MyVector<Measure> buffer_;                                // BUFFER_DIM posizioni, usate in modo circolare
    std::size_t head_;                                        // posizione della misura piu' vecchia
    std::size_t count_;                                       // misure presenti (0..BUFFER_DIM)
};

// stampa l'ultima misura salvata (senza rimuoverla); se il buffer e' vuoto stampa un avviso
std::ostream& operator<<(std::ostream& os, const InertialDriver& driver);

#endif // INERTIAL_DRIVER_H

Esercizi su questo argomento

Teoria collegata