Maurizio Tomasi
Martedì 8 Ottobre 2024
La spiegazione dettagliata degli esercizi si trova qui: carminati-esercizi-03.html.
Come al solito, queste slides, che forniscono suggerimenti addizionali rispetto alla lezione di teoria, sono disponibili all’indirizzo ziotom78.github.io/tnds-tomasi-notebooks.
Vettore
in una classe
template
(da consegnare)vector
(da consegnare)vector
e visualizzazione dei dati
(da consegnare)assert
Dobbiamo verificare che la classe Vettore
funzioni come
atteso:
void test_vettore() {
{ // New scope
Vettore<char> v{}; // Default constructor, no elements
assert(v.GetN() == 0); // The vector must be empty
}
{ // New scope: I can declare again a variable called `v`
Vettore<int> v(2);
assert(v.GetN() == 2);
v.SetComponent(0, 123); // Test both SetComponent and operator[]=
v[1] = 456;
assert(v.GetComponent(0) == 123);
assert(v.GetComponent(1) == 456);
v.Scambia(0, 1);
assert(v.GetComponent(0) == 456);
assert(v[1] == 123);
}
cerr << "Vettore works as expected! 🥳\n";
}
Questo è il test per l’esercizio 3.1, che usa
std::vector
; adattatelo poi per l’esercizio 3.2.
bool are_close(double calculated, double expected, double epsilon = 1e-7) {
return fabs(calculated - expected) < epsilon;
}
void test_statistical_functions(void) {
{ // New scope
std::vector<double> mydata{1, 2, 3, 4}; // Use these instead of data.dat
assert(are_close(CalcolaMedia<double>(mydata), 2.5));
assert(are_close(CalcolaVarianza<double>(mydata), 1.25));
assert(are_close(CalcolaMediana<double>(mydata), 2.5)); // Even
}
{ // New scope: I can declare again a variable named `mydata`
std::vector<double> mydata{1, 2, 3}; // Shorter
assert(are_close(CalcolaMediana<double>(mydata), 2)); // Odd
}
cerr << "Statistical functions work as expected! 🥳\n";
}
Nell’esercizio 3.2 è richiesto l’uso di ROOT, una libreria di funzioni per il calcolo scentifico che noi useremo per generare grafici.
Chi usa le macchine del laboratorio, dovrebbe averlo già installato.
Chi usa il proprio computer… Auguri! Sotto Linux e Mac dovrebbe essere relativamente facile installarlo, sotto Windows la cosa è più complessa.
Di tutti gli esercizi che farete questo semestre, solo il 3.2 richiede obbligatoriamente di usare ROOT. (Neppure i temi d’esami dello scritto hanno mai imposto, né imporranno, l’uso di ROOT).
Nelle prossime lezioni vi mostrerò una libreria per produrre plot, che è semplice da installare sia su Windows che Linux che Mac.
cout
e cerr
Quando si scrive a video, si può scegliere se usare
cout
o cerr
.
Le regole da seguire sempre sono le seguenti:
Usare cerr
per scrivere messaggi.
Usare cout
per scrivere il risultato di
conti.
La differenza è che quanto viene scritto su cerr
finisce sempre sullo schermo, anche se da linea di comando si usano gli
operatori di reindirizzamento >
e
|
.
Ci sono anche altre differenze; vi basti sapere che per stampare
un progress
indicator sul terminale è sempre meglio usare
cerr
.
cout
e cerr
cout
e cerr
Con l’esempio seguente, è possibile usare il reindirizzamento:
cout
e cerr
Se non avessimo usato la distinzione tra cout
e
cerr
ma avessimo scritto tutto su cout
,
comandi come sort
avrebbero mescolato risultati e
messaggi:
$ esempio | sort -g
1.75123
1. Leggo i dati da file...
2.78534
2. Stampo a video i risultati:
3. Programma completato
5.91573
In generale, stampate su cerr
tutto ciò che bisogna
mostrare all’utente subito, e non ha senso salvare in un file
per essere eventualmente guardato dopo.
Esistono due tipi di errori in un programma:
È importante gestire i due casi in modo diverso, perché l’azione più appropriata dipende dal contesto.
Questo codice ha un errore molto comune:
// The `<=` is wrong! It should be `i < ssize(v)`
for(int i = 0; i <= ssize(v); ++i) {
v.setComponent(i, 0.0);
}
Il fatto di leggere oltre la fine dell’array è un errore imputabile a chi ha scritto il programma, non a chi lo usa.
In questo caso assert
è la soluzione migliore,
perché dice al programmatore dove si trova la linea di codice da
correggere (se
si compila con -g3
).
$ ./myprog
vettore.hpp:34:void Vettore::setComponent(int pos, double value):
Assertion `pos < n_N` failed
Aborted (core dumped)
$ catchsegv ./myprog | c++filt
…
… (lots of output)
…
Backtrace:
/home/unimi/maurizio.tomasi/vettore.hpp:34(void Vettore::setComponent(int pos, double value))[0x40075f]
/home/unimi/maurizio.tomasi/vettore.hpp:79(void Vettore::set_array_to_zero())[0x40075f]
/home/unimi/maurizio.tomasi/test.cpp:15(main)[0x400796]
/lib64/libc.so.6(__libc_start_main+0xf3)[0x7fbb807d14a3]
??:?(_start)[0x40067e]
…
… (lots of output)
…
Queste informazioni sono inutili all’utente, ma molto preziose al programmatore!
Se l’errore è causato dall’utente, si dovrebbe stampare invece un messaggio d’errore chiaro, che gli consenta di riparare all’errore.
Primo esempio:
$ ./myprog
Inserisci il numero di iterazioni da compiere: -7
Errore, il numero di iterazioni deve essere un numero positivo
$
Secondo esempio:
$ ./myprog data.dat
Errore, non riesco ad aprire il file "data.dat"
$
Non è sempre immediato distinguere tra i due tipi di errore.
Ad esempio, come facciamo a sapere se nella funzione
Vettore::setComponent
un indice fuori dall’intervallo
stabilito è colpa del programmatore o dell’utente?
È sempre bene documentare che tipo di parametri vuole una
funzione, e usare assert
: in questo modo se si passano
parametri sbagliati, la colpa è per forza del programmatore!
Gli errori diretti all’utente andrebbero stampati solo nel
main
o in pochi altri posti; mai in funzioni di
basso livello, che potrebbero essere invocate all’interno di
un’interfaccia grafica anziché da terminale.
In questo esempio, la documentazione di setComponent
dice quali sono i valori accettabili per i
, quindi se il
parametro è sbagliato la colpa è di chi lo invoca.
// Set the value of an element in the array
//
// The index `i` *must* be in the range 0…size()-1
void Vettore::setComponent(int i, double value) {
assert(i < size());
m_v[i] = value;
}
int main() {
int position;
Vettore v(10);
cerr << "Inserisci la posizione del vettore: ";
cin >> position;
if (position >= ssize(v)) {
cerr << "Errore, la posizione deve essere < " << ssize(v) << endl;
exit(1);
}
v.setComponent(position, 1.0);
}
Di conseguenza, il programmatore è «costretto» a verificare la
correttezza del numero passato dall’utente nel main
prima
di invocare v.setComponent
.
Storicamente, il C++ ha permesso da sempre di dichiarare e
contemporaneamente inizializzare le variabili usando =
e le
parentesi ()
per i costruttori:
Questa sintassi è stata mutuata dal linguaggio C, ma ne esiste una più recente e più sicura.
Il C++11 implementa la uniform initialization, che si
usa impiegando le parentesi graffe {}
anziché l’uguale
=
e le parentesi tonde ()
:
Analogamente, nei cicli for
si può scrivere
così:
Inizializza una variabile al valore di default con
{}
:
Sono vietate le conversioni di tipo, spesso fonti di errori:
È possibile inizializzare array dinamici usando una sola riga:
La sintassi può essere usata anche per invocare costruttori di classi (che abbiamo già visto nella lezione precedente):
Questi vantaggi sono evidenti solo se ci si abitua ad usare la uniform initialization ovunque. Abituatevi quindi da subito!
La uniform initialization previene il problema del most vexing parse:
Usando le parentesi graffe il problema sparisce, e c’è anche simmetria con l’inizializzazione di tipi base del C++:
Questo è uno dei più comuni errori degli studenti di TNDS.
In uno degli esercizi che faremo nelle prossime settimane, il codice richiedeva da linea di comando i valori a e b degli estremi di un intervallo [a, b], nonché la precisione \varepsilon richiesta per un calcolo.
Uno studente aveva implementato il codice nel main
così:
non accorgendosi di aver usato atoi
(che restituisce un
intero) anziché atod
.
Il programma quindi andava in crash quando lo si invocava con la
linea ./esercizio 0.1 0.2
, perché sia 0.1
che
0.2
erano arrotondati a 0
e l’intervallo aveva
quindi ampiezza nulla!
Se lo studente avesse usato la uniform initialization in questo modo:
il compilatore avrebbe segnalato che nelle prime due righe c’era un
errore, perché atoi
restituisce un intero ma sia
a
che b
devono essere
double
!
int
e unsigned int
In C++ esistono gli interi con segno (char
,
short
, int
, long
,
long long
) e quelli senza segno (in cui si mette
unsigned
davanti al tipo).
Ovviamente la differenza sta nel fatto che gli interi con segno
(int
) ammettono anche numeri negativi:
Tipo | Minimo | Massimo |
---|---|---|
int |
−2147483648 | 2147483647 |
unsigned |
0 | 4294967295 |
int
e unsigned int
Usando interi con segno, molti algoritmi diventano più semplici (es., scandire gli elementi di un array a ritroso).
Citazione da Google Coding Guidelines:
You should not use the unsigned integer types such as uint32_t, unless there is a valid reason such as representing a bit pattern rather than a number, or you need defined overflow modulo 2^N. In particular, do not use unsigned types to say a number will never be negative. Instead, use assertions for this.
Preferite quindi sempre int
a
unsigned
!
for
su arrayLa classe std::vector
(esercizio 3.1) implementa un
metodo .size()
che restituisce la dimensione degli array
come un intero senza segno di tipo int
.
Questo è apparentemente sensato: dopotutto, un vettore non può avere un
numero negativo di elementi, no?
L’uso di unsigned
per le dimensione degli array
produce però più problemi di quanti ne risolva! Per questo motivo,
linguaggi più moderni scoraggiano l’uso di unsigned
(Kotlin
2.0, Nim, Julia…), quando addirittura non lo vietano (Kotlin
1.0).
In giro per Internet ci sono ancora moltissimi esempi di codice
che usano vector::size()
.
Fortunatamente, dal C++20 è disponibile la funzione
ssize()
, che funziona su vettori e altri tipi della STL e
restituisce sempre un int
:
Di conseguenza, in questo corso non usate mai
numeri unsigned
per iterare sugli elementi dei vettori.
Usate sempre int
e la funzione ssize()
! (Ma
ovviamente dovete usare un compilatore
recente…)
La classe std::vector
è un tipo di
container, ossia un «contenitore» di altri oggetti.
Oltre alla classe std::vector
, la libreria standard
C++ fornisce una serie di funzioni come sort
,
find_first_of
e min
che possono operare su un vettore o un altro tipo di container
(es., std::array
,
std::list
,
std::stack
,
std::set
,
etc.)
In tutti questi casi, non si deve passare alla funzione la
variabile di tipo std::vector
, ma una coppia di
iteratori.
Gli iteratori si trovano sempre in coppia, e rappresentano un’intervallo di elementi consecutivi. Se il primo elemento è x e l’elemento dopo l’ultimo è y, la coppia di iteratori x, y corrisponde all’intervallo
[x, \ldots, y)
Il primo elemento di un std::vector
si ottiene
mediante il metodo std::vector::begin()
,
che restituisce un iteratore (per ottenere il primo elemento,
usare std::vector::front()
).
Per l’ultimo elemento si usa std::vector::end()
(iteratore) e std::vector::back()
(l’elemento stesso).