Esercizio - InertialDriver con buffer circolare (prova intermedia 2025-26)
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
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è inindex(count_). Se il buffer non è pienocount_aumenta. Se è pienoindex(count_)coincide conhead_: si sovrascrive la più vecchia ehead_avanza di una posizione, perché la nuova più vecchia è la successiva.pop_front: copia la misura inhead_inout, poihead_avanza ecount_diminuisce.clear_buffer: bastahead_ = 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.
#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#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_HPPPunti da notare:
- 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 →): costruttore di default, costruttore con dimensione (
explicit), copia profonda, assegnamento di copia, spostamento, assegnamento di spostamento, distruttore condelete[]. - L'assegnamento di copia usa il copy-and-swap: si copia in un oggetto locale (se la copia fallisce
*thisè intatto), poi si scambiano i puntatori conswap, che non lancia, e il distruttore della copia libera il vecchio buffer. Funziona anche per l'auto-assegnamento. Vedi la strong guarantee in 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) →. reserve(n)non fa nulla se la capacità basta; altrimenti alloca prima il nuovo buffer, copia, libera il vecchio.operator[]non controlla;at()controlla e lanciaout_of_range: sono i "due overloading" richiesti, ciascuno nella versioneconste nonconst.- Il template ha le definizioni in un
.hppincluso in fondo all'header: il compilatore deve vedere i corpi dove il template viene istanziato (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 →
3. InertialDriver
#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#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 graffebuffer_{BUFFER_DIM}si sceglierebbe un eventuale costruttore ainitializer_list; qui non esiste ma le tonde evitano ambiguità;std::copycopia i 17 elementi tra array; unaforesplicita andrebbe ugualmente bene;index(k)è l'unico punto con l'aritmetica modulare: una sola funzione privata, nessun%ripetuto;size()eempty()non erano richieste ma sono "ogni altra funzione ritenuta necessaria" (servono almainper i test);get_readingrestituisce unconst Reading&, senza copiare, ed èconstperché non modifica il driver.
4. Il main di test e l'uscita
#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):
#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 limiti5. 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 solomain,README.txtcon 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 condelete[]), nessun accesso fuori dai limiti (indexcalcola sempre moduloBUFFER_DIM); - nomi esatti delle funzioni (
push_back,pop_front,clear_buffer,get_reading) econstcorretti; - magic number: 17 e 5 solo nelle costanti
N_SENSORSeBUFFER_DIM; - stile: funzioni brevi, commenti utili, un
.he un.cppper 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:
InertialDriverriceve misure da 17 sensori (lettura = 6double:yaw_v,yaw_a,pitch_v,pitch_a,roll_v,roll_a; misura = array C di 17 letture) in un buffer circolare diBUFFER_DIMmisure gestito conMyVector. Funzioni con nomi esatti:push_back,pop_front,clear_buffer,get_reading(0..16)della più recente,operator<<(ultima misura). Cartelleinclude/esrc/,README.txt,mainin file a parte, nessun eseguibile. - Scelte:
struct Reading(nessun invariante);struct Measure { Reading reading[N_SENSORS]; }perchéMyVector<T>deve copiare iTe un array nudo non è assegnabile; costantiN_SENSORS = 17eBUFFER_DIMconconstexpr; parametri riferimento a array (const Reading (&)[N_SENSORS]), così la dimensione è nel tipo. - Anello:
MyVector<Measure> buffer_(BUFFER_DIM), indicihead_(più vecchia) ecount_; la k-esima misura è in(head_ + k) % BUFFER_DIM.push_backscrive inindex(count_): se pieno coincide conhead_(sovrascrive la più vecchia) ehead_avanza.pop_frontcopia dahead_, poi avanzahead_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); duepop_frontrestituiscono 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_backraddoppia la capacità;operator[]senza controllo,at()conout_of_range.- Nucleo del codice:
#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), nientefriendné globali. Errori tipici: array come parametro (perde la dimensione),%ripetuti, sovrascrivere senza avanzarehead_,get_readingsenza controllo.Codice (inertial/include/inertial_driver.h):
#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