Sei sulla pagina 1di 58

Programmazione

Camilla Berti
July 2022

1 Cose utili
• explainshell.com // sito che spiega comandi della shell.

• ”Run and debug” su VS Code fa vedere cosa succede passo passo, utile se
dobbiamo capire cosa non va nel nostro codice.
• LiveShare su vs code utile per il progetto.
• -Wall -Wextra -fsanitize=address quando compiliamo, il sanitize fa vedere
se ci sono problemi al runtime.

• Per vedere se un comando da terminale è andato a buon fine echo $?, se


restituisce 0 vuol dire che è andato a buon fine, è l’exit state dell’ultimo
comando eseguito.
• ”discards qualifiers” errore di compilazione, vuol dire che sto passando un
oggetto costante a una funzione che prende un oggetto non costante by
reference o ho dimenticato un const da qualche parte.
• Il prof conta il numero di assert che ci sono nel codice, quindi abbondare,
utile per capire il nostro livello di comprensione del codice.
• hacking.cpp ci sono dei cheat sheets molto carini e la beginner guide è
fatta bene.
• Non mettere using namespace std
• cppinsights può essere utile

10/25-11/08
-Divisione intera: se faccio 1/2 ho come risultato 0 perché sto facendo la di-
visione fra interi, facendo 1./2. faccio la divisione fra numeri decimali invece.
-Operatore +=: area += x 7→ area = area + x

1
Numeri floating point e tipi
Il tipo è un’etichetta che viene appiccicata a un pezzo di memoria, double è
il tipo principale per i numeri decimali. double può rappresentare un sottoin-
sieme dei numeri reali, 64 bit. Un altro tipo è float, la differenza significativa
rispetto a double è che è a 32 bit. Quando si confrontano due numeri float-
ing point meglio non usare l’uguaglianza, ma piuttosto vedere che la differenza
tra i due è < ε, questo perché non sono numeri reali. Esempio associatività:
se genero tot numeri casuali e li sommo ottengo un numero, se prima di som-
marli li riordino in ordine crescente e poi li sommo vedo che ottengo un numero
diverso, ecco perché i floating point vanno maneggiati con cura. Il C++ è un
linguaggio strongly typed, fa le conversioni implicite (posso fare 1+2.3). Ci sono
conversioni implicite anche tra interi e booleani e altre cose. Se voglio forzare la
conversione in una direzione uso static cast, che fa un troncamento (esempio:
1+static cast<2.8> e ho 3 come risultato.
Utilizzo di const: rendo la variabile immutabile, tutti gli oggetti costanti vanno
dichiarati const, cosı̀ non rischio di assegnare nuovi valori a cose che voglio
restino costanti, si può mettere prima o dopo il tipo. std::string non è un
tipo primitivo (vedremo con più precisione). Una differenza fra tipi primitivi e
tipi user-defined è che se non assegno un valore i tipi primitivi sono indetermi-
nati, quelli user-defined invece posso decidere io il valore che hanno di default.

Funzioni
Una funzione è un modo per astrarre un pezzo di codice che fa qualcosa dietro
un’interfaccia ben definita, associa una serie di istruzioni a un nome e a una
lista di 0 o più parametri. Una funzione può darmi indietro un risultato (ma
non per forza).
Esempio: algoritmo Euclide: MCD tra a e b, se b=0, l’MCD è a, altrimenti a
diventa b, b diventa a % b e si ripete.
#include <iostream>
int MCD(int a, int b);
{ while(b != 0)
{ int const t=a;
a = b;
b = t % b;} return a;}
int main()
{ std::cout << MCD(a,b);}
In quest’esempio ho bisogno di introdurre la variabile ausiliaria t prima di ri-
assegnare il nuovo valore di a, perché il vecchio valore di a mi serve per fare
a % b. t la posso dichiarare const anche se ogni volta gli assegno un valore
diverso perché l’ho creata dentro al loop, quindi nel momento in cui esco dal
loop, t non esiste più. Quello che conta quindi è che t sia costante dentro al
loop. Ogni parametro nella lista dei parametri è ”tipo nome” (esempio int a, il
nome non è obbligatorio, il tipo sı̀). Quando invoco una funzione e gli passo dei
valori ”by value”, quei valori vengono copiati nella definizione della funzione.

2
Se la funzione non restituisce niente il tipo ritornato è void. In questo caso si
fa return;, ma non è obbligatorio. Una funzione può avere più return. Una
funzione può chiamare altre funzioni ed è meglio fare più funzioni corte che
si chiamano piuttosto che una molto lunga. In c++ non si possono definire
funzioni dentro altre funzioni, c’è un unico livello di definizione, quindi le devo
definire fuori dall’int main. La funzione main ha due forme:
1. int main() { } non ha parametri e restituisce un intero
2. See later
Int main è una funzione speciale, non è necessario mettere il return, ha un return
implicito che è 0 (significa convenzionalmente programma andato a buon fine,
return ̸= 0 programma non andato a buon fine).

Doctest
Per sapere che il codice che scriviamo è corretto proviamo a fare dei test sia su
situazioni ”normali” che su casi borderline che io conosco. Il testing prevede
di eseguire il codice con input ragionevoli e irragionevoli e vedere se si com-
porta secondo le aspettative. Quando si fa testing l’obiettivo è rompere il
codice. Ci concentriamo sulla forma di testing chiamato unit testing, dove
le unità di codice che stiamo testando sono ad esempio delle funzioni. Su god-
bolt è disponibile nelle libraries. Su vs code è sufficiente scaricare questo file:
https://raw.githubusercontent.com/onqtam/doctest/master/doctest/doctest.h nella
directory che contiene il nostro codice, usando wget o curl. Poi aggiungiamo
all’inizio del file C++ le righe:
define DOCTEST CONFIG IMPLEMENT WITH MAIN
#include "doctest.h" E poi definiamo dei test con questa sintassi, ad esem-
pio:
TEST CASE("Testing isqrt") {
CHECK(isqrt(0) == 0);
CHECK(isqrt(9) == 3);
CHECK(isqrt(10) == 3);
CHECK(isqrt(-1) == 0);
· · ·
}
Ogni check è un’asserzione. L’ideale sarebbe scrivere prima i test e poi il codice.

3
Memoria

Vediamo a grandi linee com’è fatto il layout in memoria di un pro-


cesso, di un programma che sta girando. Quando diciamo al sistema operativo di
eseguire un file, prende il file e lo porta in memoria, riservandogli un certo spazio
e poi lo esegue. Questa memoria è strutturata in un certo modo, qua vediamo
alcune parti: le istruzioni sono in memoria (instructions), ed è l’eseguibile. Il
programma manipola i dati, che sono di vario tipo, come letterali e costanti,
static data e heap (see later), ora vediamo lo stack. Lo stack è l’area di memo-
ria in cui viene allocata memoria legata alle funzioni, ad esempio le variabili
locali delle funzioni. Lo stack frame è un pezzo di memoria che viene allocato e
poi dedicato all’esecuzione della funzione, è gestito in a last-in first-out (LIFO)
way: l’ultimo ad essere inserito è anche il primo ad essere tolto. La dimensione
del frame è calcolata dal compilatore, che sa esattamente di quanto ha bisogno.
C’è uno stack pointer register, %rsp, che indica dove siamo arrivati.

11/25
Referenze
Partiamo da questa considerazione: se ogni volta che chiamo una funzione devo
copiare l’argomento della funzione nel parametro della funzione, questa cosa
può essere un’operazione dispendiosa. Consideriamo ad esempio una funzione
che prende una stringa e restituisce il numero di parole presenti. Se il testo
è la divina commedia e chiamo la funzione, devo copiare la stringa (la divina
commedia) all’interno del frame della funzione, ma questo è dispendioso. Voglio
evitare quindi di copiare una grande quantità di dati. Una soluzione è quella
di utilizzare le referenze. Posso immaginare la referenza come un secondo nome
dato a un oggetto esistente di un certo tipo. Vediamo un esempio:
int i = 12;
int j = 56;
int& ri = i; //ri è un altro nome per la variabile i, posso usare i e ri in
maniera intercambiabile.
ri = j //assegno a ri, quindi a i, il valore di j.
int& r; //questo non lo posso fare, la referenza nasce associata a un oggetto già
esistente e non si può riassociare ad un altro oggetto. Ritornando al problema
precedente, posso risolvere dicendo che il parametro della funzione count words
e la s non nasce come copia di qualcos altro, ma nasce come referenza a un
oggetto esistente, non è più una copia del testo e quando accedo alla s accedo

4
all’oggetto originario.
int count words(std::string& s)
{
int count = 0;
· · ·
return count;
}
int main()
{
std::string text = · · ·;
int const res = count words(text);
std::cout << res;}
Se voglio avere una referenza ”read-only”, cioè non voglio dare la possibilità a
chi riceve la referenza di fare modifiche all’oggetto originario la dichiaro const:
std::string name = "Lilla";
std::string const crname = name; // ok, crname è una referenza ”read-
only”. Non posso fare il contrario, cioè non posso creare una referenza non
costante a un oggetto che ho dichiarato const, perché altrimenti darei la possi-
bilità di modificare un oggetto che ho dichiarato const. Tornando al count words,
posso passare la stringa come const&. Seguono un paio di regole di carattere
generale da applicare quando bisogna scegliere se passare by value o by reference
in una funzione:
• Se il tipo è primitivo o ”piccolo”, li passo by value (int, double ecc)

• Tipi non primitivi li passo by const&


• Se devo modificare i parametri li basso by reference non const

Functions overloading
In c++ posso avere più funzioni che si chiamano nello stesso modo, questo si
chiama functions overloading. Le due funzioni devono differenziarsi in qualche
modo, in particolare nella lista dei parametri. Quando chiamo una funzione,
il compilatore si crea prima l’overload set, cioè l’insieme delle funzioni che hanno
lo stesso nome e poi in questo set va a cercare quella che matcha meglio con la
chiamata. Se non viene trovato il best match o non c’è un match ho un errore
di compilazione. Quando viene scelto il match non si guarda il tipo ritornato.

Conditional/ternary operator expression


expressioncondition ? expressiontrue : expressionf alse
Si usano al posto dell’if, più sintetico. Ad esempio, prendendo il mcd:
int gcd(int a, int b)
{
return (b == 0) ? a : gcd(b, a % b);
}

5
Metto come return l’espressione: se b è uguale a 0 il valore ritornato è a, altri-
menti è il valore ritornato dall’applicazione del massimo comun divisore tra b
e a % b. Comodo perché l’espressione la posso usare anche quando non posso
usare l’if, ad esempio nella chiamata di funzione fra le (). Assumiamo che il tipo
di expressiontrue e expressionf alse sia lo stesso.

Break and continue for loops


A volta è utile uscire prematuramente dal loop e posso mettere la parola chiave
break. Il continue invece si usa quando non ha senso continuare quell’iterazione
e vogliamo passare alla prossima, ma non uscire dal loop.

Object inizialition with braces


È stata introdotta come forma di inizializzazione ”universale”, sintassi alterna-
tiva che sostituisce tutte le forme di inizializzazione. Ad esempio int i{12}. Ci
sono delle situazioni in cui non si può inizializzare con le graffe, ma è la forma
raccomandata. Questa forma ha un vantaggio, protegge contro il narrowing,
cioè perdita di informazioni causata da conversioni implicite, ad esempio se uso
un double per inizializzare un float.

char
Altro tipo primitivo che rappresenta un singolo carattere, tipicamente la sua
size è 1 byte. Le constanti per un tipo char si indicano tra apici singoli 7→
’a’. Supporta le operazioni che posso fare agli interi (vedi codifica ascii, per
ogni lettera è associata un numero, utile se voglio trasformare tutte le lettere
maiuscole in minuscole ad esempio).

range-for loop
for ( range-declaration : range-expression ) statement
Forma semplificata di for loop, utile se ho un range, cioè una sequenza e voglio
scorrere tutta la sequenza. A destra dei : c’è l’espressione che denota il range, a
sinistra c’è la dichiarazione di una variabile che di volta in volta prende il valore
che sto scorrendo. Esempio 1:
for (int i : {1, 2, 3, 4, 5}) {
std::cout << i << ’ ’;
}
Ciclo che stampa la sequenza, ad ogni iterazione i assume il valore del range
successivo(prima 1, poi 2 ...). Esempio 2, ciclo che ad ogni iterazione associa
alla reference c prima H, poi e, poi l ecc, poi passo questo carattere alla funzione
toupper, che trasforma la lettera in maiuscola, poi prendo il risultato della
funzione toupper e lo assegno alla referenza c:
std::string s{"Hello!"};
for (char& c : s) {

6
c = std::toupper(c);
}
std::cout<< s;
c ogni volta viene inizializzata ad un elemento del range e l’istruzione viene
ripetuta per tutti gli elementi del range. La range-declaration ha lo stesso
tipo del tipo dell’elemento del range, meglio abituarsi a mettere una referenza
come range-declaration, eventualmente const. Nell’esempio 2 se c non era una
reference non funzionava, perché faceva una copia, non andava a modificare la
stringa originaria.

11/22
Returning a reference
Posso fare anche il return di una funzione by reference. Risulta utile farlo ma
non per il motivo per cui la passiamo a una funzione. L’importante nel caso in
cui ritorno una referenza di una funzione è che questa referenza deve riferirsi
a un oggetto che esiste dopo la fine della funzione. Infatti se l’oggetto di cui
ritorno la referenza è una variabile locale della funzione, nel momento in cui
la funzione finisce quell’oggetto non esiste già più(nello stack le variabili locali
sono nello stack frame della funzione, quando la funzione finisce lo stack frame
dedicato alla funzione non c’è più). Esempio 1:
int& add(int a, int b)
{ int result = a + b;
return result;
}
cosı̀ non va bene, perché la funzione ritorna una referenza a una variabile interna
alla funzione. Quando si chiama una funzione i parametri sono una variabile
locale. Esempio 2:
int& increment(int a)
{
++a;
return a;
}
cosı̀ invece va bene. Da notare ”int&”, che indica proprio che la funzione ritorna
una referenza. Ritornare una referenza può essere utile quando devo chiamare i
funzioni che operano sullo stesso oggetto. Invece di chiamare una funzione dietro
l’altra posso avere una scrittura più compatta usando le referenze. Quindi è utile
ritornare una referenza quando si vuole passare il risultato a funzioni successive.

Data abstraction
Vorrei poter creare dei tipi che sono rappresentativi del mio problema e operare
su quelli. Se ad esempio voglio fare un programma su un esperimento di fisica
delle particelle vorrei poter manipolare le particelle, non double o interi, cosı̀

7
da rendere il mio codice più espressivo. Il C++ è nato proprio per costruire
astrazioni di dati ”leggeri”, come sono i tipi primitivi. Il meccanismo principale
sono class o struct e ci danno la possibilità di costruire dei tipi composti a partire
da tipi fondamentali o da altri tipi composti. Proviamo a costruire un tipo che
possa rappresentare i numeri complessi. Per i tipi user-defined di solito si usa
la lettera maiuscola, quindi la struttura di base è:
struct Complex {
...
};
Dopo voglio usare degli oggetti di tipo complex come uso gli interi o i double
e applicare delle funzioni, come la norma o la radice quadrata. Definisco un
numero complesso come fatto da due double, i miei data member:
struct Complex {
double r;
double i;
};
Per calcolare la norma al quadrato devo fare r2 + i2 , quindi nella mia funzione
norm2 che prende un numero complesso come parametro devo prendere la parte
r e i, la sintassi per accedere ai dati membri di un oggetto di una classe/struct
è con il punto .:
double norm2(Complex c){
return c.r * c.r + c.i * c.i;
}
Quindi l’operatore dot permette di accedere a un membro, una sottoparte, di
un oggetto di una classe/struct. Posso anche passarlo by const&, non cambia
niente.

Operator overloading
Posso definire delle operazioni sui tipi user-defined. c1 = c2, questo lo fa il
compilatore, questa operazione invece c1 == c2 la dobbiamo fare noi:
bool operator==(Complex const& a, Complex const& b) {
return a.r == b.r && a.i == b.i;
}
Definisco anche la somma c1 + c2:
Complex operator+ (Complex const& a, Complex const& b){
return Complex{a.r+b.r && a.i+b.i};
}
Questa cosa va sotto il nome di operator overloading: posso definire gli
operatori se voglio fare delle matrici, dei polinomi ecc. Sintatticamente si fa
attraverso la definizione di funzioni appropriate: dato un generico operatore
@, il nome della funzione è operator@. Se definiamo degli operatori, devono
replicare il comportamento dell’operatore originario. Definita la classe Cmplex,
possiamo creare una funzione che risolve l’equazione ax2 + bx + c, il risultato
sarà una struct che contiene due numeri complessi e ogni numero complesso sarà
una struct di due double.

8
auto
Quando dichiaro una variabile di tipo auto e specifico un inizializzatore sto
dicendo al compilatore ”guarda, decidi tu il tipo di questa variabile in base
all’espressione che ti do per inizializzare questa variabile”. Esempi vari:
auto z; // error, no initializer
auto i = 0; // int
auto const f = 0.F; // float const
auto r = i + f; // float
auto c = Complex{1,2}; // Complex
auto& rc = c; // Complex&
auto const& cri = i; // int const&
auto j = cri; // int - auto non deduce una referenza,
viene fatta una copia, questo j è intero, glielo devo dire esplicitamente
se voglio una referenza.
auto& g = f; // float const& - the constness is
preserved
auto& rcr = c.r; // double&
Posso usare auto anche come return di una funzione.

Data abstraction pt. 2


Un meccanismo come quello precedentemente descritto di astrazione dati non è
sufficientemente potente. Supponiamo che abbia scritto la mia struct Complex e
mi accorgo che i numeri complessi li posso rappresentare anche in forma polare:
struct Complex {
double rho;
double theta;
};
cosı̀ tutte le funzioni che avevo scritto diventano sbagliate, se non altro perché
ho cambiato i nomi delle variabili e tutto il codice deve cambiare. Inoltre non
tutti i valori di ρ e θ sono validi: ρ ≥ 0 e θ ∈ [0, 2π). Quindi vorrei aver
maggiore isolamento tra il ”client code”, cioè le funzionalità implementate sopra
le mie strutture dati. Vorrei che fossero più indipendenti dalla scelta della
rappresentazione interna di quella struttura dati e vorrei fare in modo che sia
sempre vera una certa relazione tra i miei dati membri, perché non è detto che
tutte le combinazioni di valori siano validi. L’ideale è che la rappresentazione,
i miei dati membri, siano privati, da fuori non mi interessa. Questo aspetto
della riservatezza della rappresentazione va sotto il nome di encapsulation.
L’interfaccia da presentare all’esterno sono delle funzioni intimamente legate
alla struct/alla classe. La mia classe Complex diventa quindi:
class Complex {
private:
double r;
double i;
public:

9
//funzioni associate(si chiamano member functions, o metodi)
};
Classe e struct sono sostanzialmente sinonimi, c’è una differenza:in una classe se
non scrivo niente all’inizio di default è private, in una struct se non scrivo niente
di default è public. Per costruire un oggetto di tipo complex facendo Complex
c{1.,2.} ora ho un errore, stessa cosa se provo a implementare la funzione norm2
come prima.

Construction
Quando definisco un oggetto di un certo tipo, sto costruendo un oggetto di quel
tipo. Il costruttore è uno strumento, una funzione speciale, associata a una
classe, che viene chiamata quando cerco di costruire un oggetto di quella classe.
Il costruttore non ha un tipo di ritorno, è una funzione di inizializzazione. La
funzione ha lo stesso nome della classe. All’interno della classe Complex metto il
costruttore, che permette di fare le verifiche sui valori e inizializzare un oggetto
di tipo Complex, questo è il modo canonico:
class Complex {
private:
double r;
double i;
public:
Complex(double x, double y)
: r{x}, i{y} //member initialization list
{ //niente altro da fare in questo caso }
...
};
Complex c(1.,2.);
Spesso si trova anche questo, ma meglio abituarsi alla sintassi scritta sopra:
Complex(double x, double y)
{
r = x;
i = y;
}

11/29
Private representation, public interface
Rispetto a struct, class ci dà più l’idea della rappresentazione divisa tra pubblica
e privata. Riprendendo l’esempio precedente, l’implementazione che avevo fatto
prima per la funzione norm2 non va più bene, perché non posso più accedere alla
parte reale o immaginaria di un numero complesso direttamente. L’accesso alla
parte privata è filtrata attraverso le funzioni, che sono l’interfaccia della parte

10
privata verso il mondo esterno. Quindi devo scrivere all’interno della classe delle
funzioni (metodi) che mi facciano da tramite per accedere alla parte privata. Im-
plementiamo le member functions che restituiscono parte reale e immaginaria:
class Complex {
private:
double r;
double i;
public:
Complex(double x, double y)
: r{x}, i{y} { }
double real() { return r;}
double imag() { return i;}
};
double norm2(Complex c) {
return c.real() * c.real() + c.imag() * c.imag();
}
Da fuori quindi non posso accedere direttamente alla parte privata, ma lo posso
fare solo attraverso le member functions. Tipicamente quello che viene tenuto
privato è la rappresentazione del tipo, invece nella parte pubblica metto tutte le
funzioni che mi servono per poter manipolare un oggetto di quel tipo. I metodi
possono ovviamente anche modificare un oggetto, ad esempio:
class Complex {
public:
void add(Complex const& other) {
r += other.r;
i += other.i;
}...};
Complex c{1.,2.};
Complex d{3.,4.};
c.add(d);
c.real(); // 4.
Posso costruire anche dei numeri complessi al volo in questi due modi:
c.add(Complex{3.,4.});
c.add({3.,4.});
Questo può causarmi dei problemi, ora vediamo perché. Torniamo ai metodi
real e imag e passiamo a norm2 il numero complesso by const&:
class Complex {
private:
double r;
double i;
public:
Complex(double x, double y)
: r{x}, i{y} { }
double real() { return r;}
double imag() { return i;}
};

11
double norm2(Complex const& c) {
return c.real() * c.real() + c.imag() * c.imag(); //errore
}
Mi dà errore perché io potrei andare a modificare l’oggetto complesso dentro
le funzioni real e imag, quindi il compilatore non me lo fa fare, perché poten-
zialmente la funzione potrebbe andare a modificare l’oggetto. La tecnica per
dire al compilatore ”guarda ti garantisco che questo metodo non va a modificare
l’oggetto su cui viene chiamato” (come in questo caso) è specificando la parola
chiave const in questa posizione:
class Complex {
private:
double r;
double i;
public:
Complex(double x, double y)
: r{x}, i{y} { }
double real() const { return r;}
double imag() const { return i;}
};
double norm2(Complex const& c) {
return c.real() * c.real() + c.imag() * c.imag();
}
Quando i metodi non modificano gli oggetti su cui vanno chiamati vanno
dichiarati const. Ricordiamoci che va preservata la ”const correctness”: l’errore
di prima va risolto dichiarando const le funzioni, non togliendo il const da
double norm2(Complex const& c). Il c++ permette il function overloading
anche nei metodi: posso avere più metodi che hanno tutti lo stesso nome, ma
si distinguono per alcune delle caratteristiche. Oltre alla lista dei parametri
un’altra differenziazione qui è la constness. Ci sono situazioni in cui funzioni
hanno lo stesso nome e gli stessi parametri e differiscono solo perché una è const
e l’altra no. Oggetto const chiama metodi const, oggetto non const chiama di
preferenza metodo non const ma può anche chiamare metodo const (giuro che
ha senso). Esiste una consuetudine di chiamare le variabili della parte privata
con in fondo, quindi r e i .

Member vs free function


Se devo scrivere una funzionalità per la classe complex, tipo norm2, la posso
implementare come metodo, e accede direttamente alla parte privata, o come
funzione esterna dalla classe, una cosiddetta free function:
class Complex {
public:
double norm2() const {
return r * r + i * i; //member function
}...};
Oppure:

12
double norm2(Complex const& c) { //free function
return c.real() * c.real() + c.imag() * c.imag();
}
Dato un certo
Complex c{...};
c.norm2(); // chiama la funzione membro
norm2(c); //chiama la free function
Qual è più conveniente usare? Supponiamo di avere ad esempio 50 funzioni
membro e decido di cambiare la rappresentazione interna, da cartesiana a po-
lare. A questo punto preferirei avere 50 free function. La linea guida è che la
parte pubblica di una classe idealmente deve fornire un’interfaccia sicura (es-
empio voglio un’interfaccia che mi consenta di mantenere ρ ≥ 0) e efficiente, ma
tendenzialmente minimale. Perché se ho una classe con molti metodi e tocco
la parte privata devo andare a rivedere tutti i metodi, quindi meglio preferire
se possibile le free functions, cosı̀ posso andare ad estendere la funzionalità di
una classe senza modificare codice esistente. Anche perché se ereditiamo questa
classe fatta da un altro e voglio implementare delle funzionalità in più, lo devo
necessariamente fare esternamente.

Construction pt. 2
Vorrei, riguardo alla costruzione, poter fare anche queste cose:
class Complex {
private:
double r;
double i;
public:

};

Complex c1{1.,2.};
Complex c2{1.}: //intendendo {1.,0.}
Complex c3; //o {}, intendendo {0.,0.}.(Nota: non
posso usare le () qui. Perché se scrivo Complex c() intendo una dichiarazione
di funzione che ritorna un numero complesso e non prende parametri, è am-
biguo). Di fatto sto cercando di costruire un numero complesso in 3 modi
diversi, quindi definisco 3 costruttori: uno con 2 parametri, il secondo prende
un solo parametro e il terzo, il costruttore che non prende parametri, si chiama
default constructor.
class Complex {
private:
double r;
double i;
public:
Complex(double x, double y) : r{x}, i{y} {}
Complex(double x) : r{x}, i{0.} {}

13
Complex() : r{0.}, i{0.} {} //default constructor
Un modo più ganzo di farlo è usare il delegating constructor, il concetto è
quello di chiamare una funzione dentro un’altra funzione:
class Complex {
private:
double r;
double i;
public:
Complex(double x, double y) : r{x}, i{y} {}
Complex(double x) : Complex{x., 0.} {} //Delegating constructor
Complex() : Complex{0., 0.} {} //Delegating constructor
Un’altra tecnica ancora è di dare un valore di default ai dati membri in modo
che se non lo riassegno viene dato il valore di default:
class Complex {
private:
double r = 0.;
double i = 0.;
public:
Complex(double x, double y) : r{x}, i{y} {}
Complex(double x) : r{x} {}
Complex() {} //analogamente, anzi meglio scrivere Complex() = default;
Una raccomandazione forte sullo scopo del costruttore è che deve inizializzare i
dati membri e fare in modo che valga alla fine della costruzione il ”class invari-
ant” (lo vedremo meglio). Se non ci sono costruttori il costruttore di default
viene generato automaticamente dal compilatore.

std::string
std::string è una classe definita nella standard library, ci sono molti metodi:
posso chiedere quanti caratteri ha dentro con size() o se è vuota empty(),
posso accedere al primo o all’ultimo carattere back(), front() o un certo
carattere []. Su cpp reference si possono vedere i vari metodi. Il push back
aggiunge un carattere in fondo. Esercizio insieme di Mandelbrot (soluzione su
github Giacomini).

12/06
explicit constructor
Abbiamo visto che definita la classe, posso costruire un numero complesso al
volo cosı̀: norm2(Complex{5.,6.};.
Se faccio norm2(1.); posso decidere se voglio che funzioni o no. Posso calcolare
la norma di un numero reale? Posso decidere, diciamo in questo caso che vorrei
permettere al nostro sistema di fare questa cosa, vedendo un numero reale come
un numero complesso che ha parte immaginaria 0. Quindi passo un double che

14
viene convertito al numero complesso corrispondente su cui viene applicata la
funzione norm2. C’è un costruttore di complex che prende un double, quindi il
compilatore fa una conversione implicita. Ci sono situazioni in cui non voglio
che accada questa cosa. Per prevenire, c’è una parola chiave, che è explicit,
da associare al costruttore, che dice al compilatore che quando voglio costruire
un numero complesso lo dico esplicitamente:
class Complex{
private:
double r ;
double i ;
public:
explicit Complex(double x, double y) : r{x}, i{y} {}
Complex(double x) : Complex{x., 0.} {}
Complex() : Complex{0., 0.} {} Ora se faccio norm2(1.) ho un errore di
compilazione. La conversione implicita entra in gioco anche in questo caso:
Complex operator+(Complex const& c1, Complex const& c2) {
return {c1.real() + c2.real(), c1.imag() + c2.imag()};
} Non ho specificato che voglio un Complex come return. Se metto explicit
il compilatore non accetta più questa cosa e devo scrivere Complex. Il com-
pilatore, in assenza di explicit, può usare una catena di conversioni: se scrivo
norm2(1), l’intero viene convertito a double e poi a complesso. Gli operatori
binari simmetrici meglio metterli come free function. Inoltre per prudenza è
sempre meglio mettere explicit al costruttore e casomai levarlo dopo.

operator@ in terms of operator@=


Tipicamente un operatore simmetrico è spesso implementato in termini di una
funzione membro @=. Per esempio l’operatore + è implementato in termini
di operatore+=, che è definito come membro. Ad esempio, sempre nella classe
complex:
void moltiplica per(double d){
r = r * d;
i = i * d;
} e poi dato un complex c1, faccio c1.moltiplica per(2.). Per definizione, la
scrittura di prima si può scrivere come:
void moltiplica per(double d){
r *= d;
i *= d;
} Posso fare la stessa cosa anche per la somma e definire l’operatore +=:
Complex& operator+=(Complex const& other) {
r += other.r ;
i += other.i ;
return *this;
}
e dati due numeri complessi posso fare c1 += c2;. Notare che per convenzione
non si ritorna void ma si ritorna l’oggetto stesso by reference e per indicare che

15
si ritorna l’oggetto stesso si usa la scrittura *this (poi vedremo meglio cosa
vuol dire). Ora possiamo implementare l’operatore + cosı̀:
Complex operator+(Complex const& c1, Complex const& c2)
{
auto result = c1;
return result += c2;
} (mi serve la variabile ausiliaria result e non posso fare direttamente return c1
+= c2 perché ho passato c1 come const&).

Numeri razionali: class invariant, assert


Abbiamo detto che lo scopo del costruttore è fare in modo che alla fine del corpo
del costruttore valga l’invariante di classe. L’invariante di classe è una relazione
fra i dati membri, la parte privata della classe, che deve valere sempre. Cosı̀ se
ci troviamo davanti ad esempio un numero razionale siamo sicuri di poterlo ma-
nipolare stando tranquilli. Usiamo l’esempio dei nuneri razionali, poi l’esempio
è simile ad avere un numero complesso in forma polare, dove non tutti i valori
sono accettati. Devo fare in modo che la rappresentazione interna sia valida.
Rappresentiamo le frazioni con due interi. Non tutte le combinazioni di interi
sono validi: la frazione deve essere irriducibile, se il numero è negativo il segno
è mantenuto dal numeratore e il denominatore deve essere sempre maggiore di
0. Quindi quando costruisco un numero razionale devo garantire che il numero
razionale soddisfi queste tre relazioni. Cosı̀ se voglio confrontare ad esempio due
numeri razionali, posso confrontare semplicemente numeratore e denominatore,
se non fossero primi tra loro sarebbe più complesso. Per implementare questa
classe, partiamo dai test:
#define DOCTEST CONFIG IMPLEMENT WITH MAIN
#include "doctest.h"
#include <numeric>
#include <cassert>
class Rational {
private:
int n ;
int d ;
public:
Rational(int n = 0, int d = 1) : n {n}, d {d} {
auto const g = std::gcd(n , d ); //riduco ai minimi termini
n /= g;
d /= g;
if (d < 0) { //metto a posto il segno
d = -d ;
n = -n ;
assert(std::gcd(n , d ) == 1 && d >0);
}
int num() const{ return n ; }
int den() const{ return d ; }

16
};
bool operator==(Rational const& r1, Rational const& r2) {
return r1.num() == r2.num() && r1.den() == r2.den()}

TEST CASE("Testing rational"){


Rational r0{};
Rational r1{5};
Rational r2{-3,2}; //cosı̀ vedo che il costruttore funziona.
CHECK(r2.num() == -3);
CHECK(r2.den() == 2);
CHECK(Rational{2,2} == Rational{1,1});
CHECK(Rational{25,10}.num() == 5);
CHECK(Rational{2,-2} == Rational{-1});
Rational r3{1,0};

Per far sı̀ che passi il test CHECK(Rational{2,2} == Rational{1,1});, devo


fare in modo che valga l’invariante di classe, andando a lavorare sul costruttore.
Per ridurre una frazione ai minimi termini guardo l’MCD e divido entrambi per
il massimo comun divisore. Per calcolare l’MCD faccio #include <numeric>.
Poi mettiamo a posto il segno, che abbiamo detto è tenuto dal numeratore, in-
troducendo l’if. Se il denominatore è 0 la costruzione deve fallire.
Riassumendo, l’invariante di classe è una relazione tra i dati membri che vincola
i valori accettati di quella classe, che deve valere sempre. Quindi il costruttore
fa in modo che quando l’oggetto è inizializzato l’invariante valga. Poi vogliamo
che l’invariante resti nel tempo: quindi quando chiamo un metodo pubblico devo
fare in modo che quando l’oggetto esce dal metodo l’invariante sia preservato:
se ho un metodo ad esempio che moltiplica due numeri razionali può darsi che
alla fine debba rifare il giochino del massimo comun divisore e del segno, cosı̀ il
prossimo metodo pubblico invocato si ritrova di nuovo l’invariante valido e può
lavorare assumendo che quella relazione valga. Durante l’esecuzione della fun-
zione l’invariante può essere rotta, ma alla fine devo ripristinare l’invariante. Nel
linguaggio, per esprimere questa relazione posso usare gli assert, le asserzioni,
che hanno lo stesso corpo delle funzioni. Posso esprimere questa relazione e
inserire nel nostro codice in punti strategici quest’espressione, cosı̀ al runtime se
l’espressione è validata il programma continua regolarmente, se non è valida il
programma muore. Se in un certo punto del nostro programma noi sappiamo che
doveva valere quella relazione e in realtà non vale, quindi non abbiamo la com-
prensione corretta del nostro codice, tanto vale morire (non noi, il programma).
Per mettere gli assert facciamo #include <cassert>. La nostra condizione
deve valere alla fine del costruttore, la esprimiamo come una relazione booleana
(vedi codice). Assert è uno strumento generico, lo posso usare non solo per rap-
presentare l’invariante di classe, ad esempio implementando questo operatore:
Rational& operator /=(int n) {
assert(n != 0);
...
}

17
Se chiamo la funzione con n=0, il programma crasha. Le assert possono essere
disabilitate per questioni di performance con g++ -DNDEBUG. Per questo
motivo quello che si mette negli assert deve essere in sola lettura, perché con
questa opzione tutto quello che c’è dentro gli assert sparisce. A livello infor-
mativo, l’assert è una macro del preprocessore. Manca la parte del codice per
cui se il denominatore è uguale a 0, la costruzione deve fallire(lo vedremo nella
lezione successiva).

12/13
L’invariante di classe deve valere alla fine del costruttore e dobbiamo fare in
modo che quando chiamiamo i metodi possiamo assumere che l’invariante valga
e dobbiamo fare in modo che alla fine dell’esecuzione della funzione l’invariante
di classe valga nuovamente ”per essere un buon cittadino”. Questo ci dà una ra-
gionevole confidenza che il nostro codice sia corretto, cosı̀ riduciamo al minimo
il tempo che passiamo a debuggare il codice. Poi non tutte le invarianti sono
esprimibili tramite codice, ma la forma mentis deve essere questa. Le assert
sono macro del preprocessore, quindi hanno delle regole sintattiche diverse da
quelle del C++:
assert(Rational{1,2} == Rational{2,4}); //errore
assert(Rational(1,2) == Rational(2,4)); //ok
assert((Rational{1,2} == Rational{2,4})); //ok

Exceptions
Dobbiamo gestire il caso in cui viene costruito un numero razionale con denom-
inatore uguale a 0. Per fare questo bisogna usare il meccanismo delle eccezioni.
Le eccezioni sono un meccanismo generale per notificare all’esterno, a chi chiama
la funzione, che si è verificato un errore. Il meccanismo sintattico è:
#include <stdexcept> ...
class Rational {
...
public:
Rational(int n = 0, int d = 1) : n {n}, d {d} {
if (d == 0) {
throw std::runtime error{"Denominator is zero"};}
Sto costruendo un oggetto di tipo runtime error che mi dice cosa è successo
e sollevo l’eccezione. Quando si parla di eccezioni ci sono tre parole chiave:
throw, try, catch. Il throw permette di lanciare un’eccezione quando è ril-
evato un problema, ci permette di creare un errore personalizzato. Il try ci
consente di definire un blocco di codice che viene testato per vedere se viene
fuori qualche errore quando viene eseguito. Il catch permette di definire un
pezzo di codice che gestisce l’errore e stabilisce il punto in cui deve continuare
l’esecuzione. Uno dei vantaggi delle eccezioni è che permettono di separare la

18
logica dell’applicazione dalla gestione degli errori.
struct E {};

auto low() {
std::cout << "1";
throw E{};
std::cout << "2"; }

auto mid() {
std::cout << "3";
low();
std::cout << "4"; }

int main() {
try {
std::cout << "5";
mid();
std::cout << "6";
} catch (E& e) {
std::cout << "7";
} }
I cout sono messi per far vedere cosa viene eseguito e cosa no.
Viene fuori 5, 3, 1, 7. Viene stampato 5, poi viene chiamata la funzione
mid, che stampa 3 e poi chiama la funzione low, che stampa 1 e poi solleva
un’eccezione (l’eccezione è un oggetto). Dopo che viene trovata l’eccezione, du-
rante l’esecuzione si va a cercare il catch che vuole quel tipo di eccezione lı̀.
Quindi esce prematuramente da low, esce prematuramente da mid, va nel main,
salta il blocco try e l’esecuzione riparte da dove viene trovato il catch. Se non c’è
nessun catch in grado di gestire l’eccezione, il programma termina. Le eccezioni
sono un meccanismo di segnalazioni di errori che interrompono il flusso di con-
trollo in corso e lo trasferiscono da qualche parte, cioè escono dalla funzione
che è in esecuzione in quel momento. Il flusso di controllo riprende dalla prima
istruzione in corrispondenza di un catch compatibile (vediamo più avanti in che
senso compatibile) con l’eccezione sollevata. main chiama mid, mid chiama low.
Low solleva un’eccezione di tipo E e al run time si vede se c’è un catch com-
patibile. Le eccezioni si sollevano by value e si fa il catch by & o by
const&. Questo da prendere come regola. Questo come ci torna utile? Di
fatto è l’unico modo per segnalare che il costruttore non riesce a portare a ter-
mine il proprio lavoro. Applichiamola a Rational: per semplicità ogni volta che
solleviamo un’eccezione possiamo usare std::runtime error. Il costruttore di
questo oggetto prende una stringa che contiene il motivo per cui viene sollevata
l’eccezione. Le eccezioni sono utili per noi perché l’oggetto non viene proprio
costruito. La classe std::runtime error ha un metodo che si chiama what() che
restituisce la stringa che ho messo nel costruttore. Classe che possiamo costruire
anche noi circa, molto easy, qualcosa tipo:
class runtime error

19
{
std::string s ;
public:
runtime error(std::string const& s): s {s} {}
std::string what() const {return s ;}
}
Le eccezioni sono classi normalissime quindi che contengono delle informazioni
che aiutano a capire il motivo del fallimento. Riassuntino:
try {
//block of code to try
throw exception; //throw an exception when a problem arise
}
catch () {
//block of code to handle errors } Un’espressione throw segnala che si è verifi-
cato un errore in un blocco try. Si può usare un oggetto di qualsiasi tipo come
operando di un’espressione throw. Questo oggetto viene in genere utilizzato per
comunicare informazioni sull’errore, possiamo anche derivare la nostra classe di
eccezioni. Ogni blocco catch specifica il tipo di eccezione che può gestire. Se
non c’è nessun errore il blocco catch viene skippato. Se non sappiamo il tipo
di throw usato nel blocco di try, possiamo usare la sintassi catch (...) { },
che gestisce ogni tipo di eccezione.

Assertions or exceptions?
Non c’è una regola generale su quando usare le asserzioni o le eccezioni, al mas-
simo linee guida. Il consiglio è essere generosi con le asserzioni e usarle per
cercare di esprimere, per quanto possibile, il nostro convincimento sullo stato
del programma. Ad esempio possiamo ammettere un assert per vedere se vale
l’invariante alla fine del costruttore e all’inizio del metodo. Vedere come ispi-
razione la classe Rational sul github di Giacomini. Posso ad esempio creare una
funzione booleana che esprime l’invariante cosı̀ dentro l’assert posso semplice-
mente mettere la chiamata di funzione. L’eccezione viene usata per segnalare
che una funzione non riesce ad eseguire il proprio compito. Esempio: mi hai
chiesto di aprire un file, ma quel file non c’è. Non usare l’assert per validare
l’input a un programma: se chiedo due numeri e voglio che il secondo sia diverso
da zero non metto l’assert, al massimo l’eccezione. Quindi per input passato
dall’esterno, niente assert.

The switch statement


Nelle situazioni in cui capitano a cascata una serie di else if a volte si può usare
lo switch statement. Abbiamo lo switch su una certa condizione e tanti casi.
Se la nostra condizione non rientra in uno di questi casi, viene assunto uno di
default (che ovviamente possiamo decidere, ma non è obbligatorio).
double compute(char op, double left, double right) {
double result;

20
switch (op) {
case ’+’:
result = left + right;
break;
...
case ’/’:
result = (right != 0.) ? left / right : 0.;
break;
default:
result = 0.;
}
return result;}
La condizione è un’espressione la cui valutazione deve dare un valore intero,
quindi non lo possiamo fare su string. Abbiamo una serie di statement introdotti
o da un’etichetta ”case” o da un’etichetta ”default”. Ogni case specifica una
costante. Tipicamente ogni statement è seguito dall’istruzione break, che va
alla prima istruzione dopo lo switch. Questo perché se non mettiamo il break
l’esecuzione continuerebbe con le istruzioni di un’altra etichetta case: quello che
si chiama ”fall through”. Il compilatore in genere dà un warning quando c’è
un fall through, anche se alle volte è voluto e il warning può essere silenziato
con [[fallthrough]] attribute. Può esserci una sola default label, che possi-
amo mettere dove vogliamo. Lo stesso statement può essere introdotto da più
label: ad esempio nel caso della divisione voglio che sia ”/” che ”%” prendano
la divisione.

02/21
C++ Compilation model
Vediamo come si possono mettere insieme dei progetti in cui ci sono tanti file.
(Nel progetto non possiamo consegnare un solo file). Abbiamo visto cosa sig-
nifica scrivere un programma, che è un modo per comunicare al computer le
azioni da compiere. Ci sono vari modi per programmare, alcune cose sono in
comune con altri linguaggi di programmazione (programmazione strutturata).
Dal punto di vista teorico i costrutti come while, for, if sono sufficienti per im-
plementare qualunque programma, ma per esprimere meglio il nostro pensiero
in termini di programma abbiamo introdotto le funzioni, che astraggono dei
pazzi di codici e poi abbiamo introdotto anche le astrazioni di dati, grazie alle
classi. Vediamo come suddividere il codice in vari pezzi e vediamo come i vari
pezzi si possono parlare tra loro, parlando del metodo corrente del C++ com-
pilation model, anche se le cose stanno per cambiare perché il nuovo standard
ha introdotto i moduli, un modo alternativo per impacchettare i componenti
software. Il modello corrente comunque sarà corrente per almeno altri 10 anni.

21
Torniamo alle origini:
#include <iostream>
#include <string>

int main(){
std::cout<<"What’s your name?"<<endl;
std::string name;
std::cin>> name;
std::cout<<"Hello " << name <<endl;
}
Un cpp con i suoi include va sotto il nome di translation unit. Quello che in
realtà passo al compilatore è la translation unit. La produzione delle trans-
lation unit (vediamo che ne possiamo avere più di una), consiste nel pren-
dere il cpp e risolvere tutti gli include, è il primo step del processo di com-
pilazione. Quindi il primo passo che mi porta dal sorgente al binario è pro-
durre una translation unit. Questo è fatto da un sottoprogramma della com-
pilazione che si chiama preprocessor. Il preprocessore scorre il file, guarda
tutti gli include e mi produce la translation unit. Per vedere il risultato di
questo step c’è quest’opzione: g++ -E hello.cpp -o hello.ii (il .ii per con-
venzione, mi serve cosı̀ lo posso aprire). Il preprocessore scorre il file, trova per
esempio #include <iostream>, va a cercare dove sa lui il file che si chiama
iostream e lo copia e incolla dentro al mio codice. Analogamente per string.
Un tipico programma c++ è com-
posto da molti file, ci sono molte
translation unit quindi è più cor-
retto questo disegnino, dove si in-
tende la compilazione ancora in
senso lato. Sorgono spontanee le
domande: come distribuisco il mio codice su più file? Se metto una classe in
un file e la uso nel main, come fa il main a sapere che esiste la classe? Questo
va sotto il nome di physical design. La figura più generale del processo di
compilazione è la seguente:

22
Su ogni translation unit giro il compilatore vero e proprio, vengono prodotti
altrettanti object files e ho un’altra fase del processo di compilazione chiamata
linking dove metto insieme tutti i pezzi, compresa la standard library e altre
librerie dove c’è codice precompilato. Gli object files sono un pezzo di binario,
uniti alle librerie esterne producono un eseguibile. Per fermarmi alla produzione
degli object files l’opzione è g++ -c hello.cpp. Per default crea il file hello.o.
C’è un’altra opzione, che è g++ -save-temps hello.cpp per tenere tutti i file
intermedi prodotti durante il processo di compilazione.

Definition vs declaration e header/source file


La definizione è una dichiarazione che definisce completamente le entità di cui è
oggetto: una definizione di funzione è quando ho la funzione e c’è scritto anche
il blocco di codice che contiene le istruzioni di cosa fa. Oppure quando scrivo
int n = 5 è una definizione. Una dichiarazione che non è una definizione è
un meccanismo sintattico che introduce il nome e eventualmente il tipo. La
dichiarazione di una classe è semplicemente class Regression. In c++ esiste
una regola, One Definition Rule che dice che un’entità può essere definita una
sola volta per ogni translation unit. In generale un’entità può essere definita
una sola volta all’interno del programma, anche se per questa generalizzazione
ci sono delle eccezioni per cui è ammesso che alcune entità siano definite in due
translation unit, a patto che le due definizioni devono essere identiche, ”token
by token”,
1. Definizioni di classe
2. Funzioni o variabili dichiarate inline
3. Templates di classi e funzioni

(ci sono anche altre cose non importanti). Il trucco è mettere la definizione
della classe in un header file che per convenzione chiamo hpp e poi includo dove
ho bisogno. Cosı̀ sono sicura che la definizione della classe in due translation
unit sia la stessa. Durante la compilazione(compilazione senso stretto, gener-
azione object file) per chiamare una funzione mi serve solo la dichiarazione della
funzione. La dichiarazione della funzione richiede che esista la dichiarazione
dei tipi coinvolti. Ad esempio per dichiarare int square(Complex), mi basta
dichiarare Complex, non definirlo. Per creare e manipolare oggetti mi serve la
definizione del tipo corrispondente. Durante il linking tutto deve essere definito
completamente. Gli header file sono il meccanismo principale per garantire che

23
dichiarazioni e definizioni siano identiche in tutte le translation unit, possiamo
esserne sicuri perché il meccanismo degli include file fa copia e incolla, easy.
Come linea generale abbiamo
• header file (hpp) che contiene la definizione delle classi con le dichiarazioni
delle funzioni membro, le dichiarazioni delle free function(esempio gli op-
eratori) e le definizioni dei template.
• Un file sorgente (cpp) dove mettiamo le definizioni delle cose che mancano:
metodi, free function e altre cose che non vogliamo esportare in altri file
che non ci servono da altre parti.

• File con i test(includono l’hpp)


Come suggerimento il file sorgente (cpp) come prima cosa fa #include name.hpp
perché nel file hpp deve essere contenuto tutto ciò di cui ha bisogno, deve es-
sere autonomo, e includendolo per primo mi assicuro di questa cosa. Cioè se
ad esempio nell’hpp ho bisogno di string, includo string nell’hpp. Quindi anche
se faccio tutto nell’hpp conviene comunque creare un file cpp in cui includiamo
l’hpp cosı̀ compilando ci assicuriamo che l’hpp sia autonomo e ci segnala se ci
siamo dimenticati degli inline(vedi sotto).

inline
Una situazione tipica prevede che l’header file contenga le dichiarazioni di fun-
zioni: nome, parametri, tipo ritornato. In alcuni casi però sono obbligato a
definire la funzione nell’header file. Pensiamo a Complex che nell’header file
ha la definizione della funzione norm2 ad esempio. Ora includo questo hpp
in dieci translation unit diverse: a questo punto la funzione norm2 è definita
in dieci translation unit diverse. Questo è una violazione della One Definition
Rule. In questi casi si usa la keyword inline, usata nella definizione di una
funzione, all’inizio (tipo inline int norm2). Questo dice al linker: ”guarda
questa funzione te la trovi in 10 translation unit ma fidati è sempre la stessa,
prendi quella che ti pare”. Non serve mettere inline quando definisco i metodi di
una classe, è implicito. Le free functions definite nell’header file, le devo mettere
inline. Se includo per esempio in un cpp due volte il file hpp e ho definito una
funzione dentro l’header file mi dà errore perché violo la ODR: non importa che
abbia messo la funzione inline, quando sono nella stessa translation unit non
posso definire una funzione due volte, la One Definition Rule viene applicata
rigorosamente in questo caso. I vantaggi di mettere inline si vedono dal punto
di vista della performance: il compilatore più codice vede, più può ottimizzare,
può fare il cosiddetto inlining. Uno svantaggio è che se modifico una cosa che ho
dichiarato inline devo ricompilare tutte le translation unit che includono quel
file.

24
02/22
Include guard
Riguarda l’errore scritto nelle righe sopra, se includo direttamente o indiretta-
mente più volte un header file nella stessa translation unit, esempio:
#include "point.hpp"
#include "regression.hpp" regression.hpp includes point.hpp Cosı̀
ci sarebbero più definizioni di una certa entità nella stessa translation unit, che
viola la ODR. Si usa il preprocessore, usiamo questo costrutto, l’include guard
all’inizio di ogni header file:
#ifndef POINT HPP
#define POINT HPP
// classes, functions, templates, ...
#endif
nel point.hpp. Quindi ora se guardiamo il file dove ci sono i due include di
point e regression il compilatore pensa cosı̀: vede point.hpp, pensa ”è definita
la macro point.hpp?” No, quindi la definisco. Poi quando trova di nuovo il
point.hpp all’interno di regression.hpp si accorge che è già stata definita, quindi
salta il pezzo che la riguarda. A volte, anche se non è standard si usa con lo
stesso significato dell’include guard con #pragma once.

Member functions defined outside the class


I metodi di una classe devono essere dichiarati dentro la classe, ma possiamo
definirli anche fuori, sia nello stesso file, ma anche in un file diverso. La sintassi
è semplice, quando la chiamo per fare la definizione devo specificare che non è
una free function, ma che è un metodo di una certa classe, in questo modo:
void Regression::add(Point const& p) {
++N ;
sum x += x ;
· · ·}
I :: sono lo scope operator. Quindi class name::method-name. Chiaramente se
la definisco nell’hpp è una funzione, quindi la devo mettere inline.

Template
Meccanismo sintattico alla base della programmazione generica, uno degli stili
di programmazione con cui può possiamo programmare in c++. Oltre alla pro-
grammazione procedurale, c’è la possibilità di astrarre dei dati. Oltre a questi
due c’è la programmazione generica e quella object oriented(che vedremo più
avanti). Riprendiamo in esame la classe Complex: cosa succede se vogliamo i
float invece che i double? Dovremmo sostituire tutto, tipo:

25
class Complex { class Complex {
double r; float r;
double i; float i;
public: public:
Complex( Complex(
double x = 0. float x = 0.F
, double y = 0. , float y = 0.F
) : r{x}, i{y} {} ) : r{x}, i{y} {}
double real() const { return float real() const { return r;
r; } }
double imag() const { return float imag() const { return i;
i; } }
}; };

Vediamo che la classe Complex è parametrica: c’è un parametro che mi dice


il tipo che voglio utilizzare internamente. La sintassi per avere i template è
questa:
template<typename FP> // (*)
class Complex {
static assert(std::is floating point v<FP>);
//vedi sotto
FP r;
FP i;
public:
Complex(
FP x = FP {}
, FP y = FP {}
) : r{x}, i{y} {}
FP real() const { return r; }
FP imag() const { return i; }
};
Complex <int> i; //grazie allo static assert mi dà errore
(*)Oppure template<class FP>. Parametro che viene ripetuto dove serve.
Class in questo contesto è sinonimo di typename. Ora Complex a questo punto
non è un tipo, è uno stampo, quindi se faccio Complex c; ho un errore, devo
scrivere Complex <double> d;, per dire qual è il parametro. Come alternativa
al compilatore posso dire Complex e{1.,1.}; e il compilatore deduce il tipo. A
questo punto, Complex double e Complex float sono due tipi diversi, se ho una
funzione che prende Complex double non posso passare un Complex float, avrei
un errore. A questo punto ci possiamo chiedere: ha senso costruire un Complex
di int? Se dico di no, esistono vari modi per proibire di costruire un Complex
con gli interi. Uno è usare uno static assert. Static assert: gli metto una con-
dizione booleana che se fallisce al compile time mi dà errore di compilazione.
Quindi un class template è un generatore di tipi che devo riempire. I tipi pos-
sono essere espliciti o dal c++17 anche impliciti. Quando scrivo il tipo tra

26
parentesi angolate faccio una ”specializzazione”. C’è la possibilità, introdotta
da c++ 20, che usa i ”concetti” per vincolare i parametri che passo durante la
specializzazione, un modo più elegante degli static assert, ma noi non lo vedi-
amo. Avere solo i class template non ci basta, perché non possiamo più fare
queste cose:
double norm2(Complex const c) { · · · } // non è più valido perché non
esiste più la classe Complex e non posso fare neanche norm2(f);. Per risolvere,
ci sono le function template:
template <typename FP>
auto norm2(Complex<FP> const& c) {
return c.real() * c.real() + c.imag() * c.imag();
}
auto nf = norm2(f);
auto nd = norm2(d);
Nota: f e d sono rispettivamente un Complex float e un Complex double. La
standard library del c++ sfrutta molto questo principio della programmazione
generica, come nei container o negli algoritmi. Quindi una function template
non è una funzione, è un generatore di funzioni. C++ supporta la possibilità
di dedurre gli argomenti delle funzioni:
Complex<float> f;
norm2(f); //deduzione dell’argomento della funzione
norm2<float>(f); //qua siamo espliciti, va bene comunque.

Non-type template parameter


Abbiamo visto come usare i template type parameters per creare funzioni e classi
che sono ”type independent”. Un template type parameter è un segnaposto che
viene sostituito di volta in volta quando si specializza il tipo. Però ci sono
anche i non-type template parameter. In questo caso il tipo del parametro è
predefinito. Quindi un template parameter non è necessariamente un tipo, può
essere ad esempio un valore. Vediamo questo esempio stupido, in cui ho una
struct dove il parametro è un numero intero, e quando cambio il numero intero
ho due tipi diversi:
template<int I> struct C · · · ;
C<2> c2;
C<3> c3; // different type

int n; std::cin >> n;


C<n> c4; //errore, il numero deve essere noto al compile time.
Vedremo un esempio più significativo e comprensibile.

27
02/28
Namespaces
Il problema che vogliamo risolvere è che tipicamente i programmi sono fatti da
moltissime righe di codice, che non sono sviluppate tutte dalla stessa persona.
Per evitare confusione o equivoci nel caso siano necessarie molte entità con nomi
simili, il c++ offre la soluzione dei namespace, una collezione di nomi raggrup-
pati per categorie. Questo spazio dei nomi ha un nome a sua volta, noi abbiamo
visto quello dello standard, std. Troviamo i nomi di tutto quello definito nella
standard library nel namespace std. I namespaces possono essere riaperti per
aggiungere nomi. Dentro un namespace ci possono essere altri namespace, per
raggruppare tutto quello che abbiamo definito in categorie. Ad esempio nel
namespace std c’è il namespace chrono, che ha tutte le cose che hanno a che
fare con il tempo. Se voglio usare qualcosa dentro questo namespace dovrei
scrivere std::chrono::qualcosa, questo talvolta può essere laborioso. Quindi ci
sono degli alias, degli altri nomi per i namespace, ad esempio per chrono c’è:
namespace ch = std::chrono;, cosı̀ se voglio sapere l’ora posso usare auto t0
= ch::system clock::now(); invece che std::chrono::system clock::now().
Un’altra cosa che possiamo fare sono le using declaration: se al posto di scri-
vere sempre std::string voglio scrivere semplicemente string, nello scope
(l’ambito dove ci sono le parentesi graffe), posso scrivere:
using std::string;
string s;
anche se cosı̀ mi espongo a conflitti. A questo proposito, non si mette using
namespace std;, perché rende visibili tutti i simboli sullo scope, crea molti
problemi. Ad esempio nei primi esercizi abbiamo definito la funzione gcd. Il
gcd però è stato introdotto nello standard, e di conseguenza rischio conflitti: se
mi va bene ho errore di compilazione, sennò viene scelta la funzione sbagliata.
Noi dobbiamo usare il namespace? Sı̀, quando scriviamo più di 100 righe di
codice è buona norma metterlo (tipo nel progetto).

Enumerations
Permette di definire un nuovo tipo, specificando, dando un nome a delle costanti.
Enumerations è il tipo, enumerator è la costante associata. Ogni enumerazione
è un tipo. I valori che possono essere assunti da una variabile enumerazione sono
ristretti ad un insieme di valori interi costanti, ad ognuno dei quali viene associ-
ato un nome. La coppia nome-costante è detta enumeratore. Per la definizione
di una enumerazione viene usata la parola chiave enum, secondo la seguente
sintassi:
enum frutta {mela, pera, banana, pesca, kiwi};// ”frutta” è l’identificativo
della enum
enum { mela, pera, banana, pesca, kiwi }; //enum senza identificativo.
Essenzialmente è un modo per creare una nuova classe al volo senza metodi o
altre cose, creo un nuovo tipo e specifico i valori che può assumere una variabile

28
di quel tipo. Come esempio stupido, se creassi un enumeratore dove all’interno
metto tutti i numeri interi possibili, allora sarebbe praticamente come int. Altro
esempio:
enum class Operator { Plus, Minus, Multiplies, Divides };
Per usare queste costanti:
auto op{Operator::Plus}; // op is of type Operator
Le enumerazioni si sposano bene con gli switch statements:
double compute(Operator op, double left, double right)
{
switch (op) {
case Operator::Plus: return left + right;
case Operator::Minus: return left - right;
case Operator::Multiplies: return left * right;
case Operator::Divides: · · · return left / right;
default: throw std::runtime error{"invalid operand"};
}
}
Una enumeration ha un underlying type, che di default è un int (in ogni caso
deve essere intero). Nel caso di operator per esempio ho plus=0, minus=1 e
cosı̀ via, partendo a contare da zero. Il senso è che ai primi 4 valori ho dato
un nome, gli altri no. C’è anche una versione meno restrittiva dell’enumeration,
che si chiama unscoped enumeration, meglio usare quella di prima però.

Hexadecimal notation
I numeri in notazione esadecimale sono rappresentati con la base 16, al posto
che la base 10. Per far ci servono 16 simboli: 0, 1, 2,... A, B, C, D, E, F.
È facile la conversione tra notazione esadecimale e binaria: ogni esadecimale
corrisponde a 4 bit, notazione più compatta. Si usano per rappresentare gli
indirizzi di memoria.

Pointers
Un puntatore ci dice dove sta in memoria un oggetto. Servono per rispondere
alla domanda: dove sta in memoria l’oggetto che ho definito? Posso immaginare
la memoria come un nastro lunghissimo di celle, ognuna con un certo indirizzo,
immaginiamo una cella di dimensione 1 byte. Un intero occupa 4 byte, quindi se
voglio sapere l’indirizzo viene fornito solo il primo. L’operatore che mi permette
di accedere all’indirizzo è &: da non confondere con l’ delle referenze: in quel
caso è attaccata a un tipo, qui è prefisso a una variabile:
int i = 25;
std::cout << &i; //0xab00
Il tipo di un indirizzo in memoria è ”puntatore a (tipo oggetto)”: la sintassi è:
int* p = &i; //pointer declarator
int* è proprio un tipo. La variabile p a sua volta è un oggetto che contiene
l’indirizzo in memoria, quindi anche p sta in memoria, quindi posso fare

29
int** pp = &p e andare avanti quanto mi pare. (noi comunque non vedremo
cose con più di un asterisco). Poi dato p che punta a i, posso riassegnarlo: int*
p = j;, dato j=1234 un altro intero. Da notare che le referenze non potevo
riassegnarle, non erano ”autonome”.
int* q = p p e q puntano allo stesso oggetto.
Dato un puntatore, come faccio a sapere qual è l’oggetto a cui punta? C’è un
operatore simmetrico, l’operatore di dereferenza, *, che applico a un puntatore
e restituisce la referenza all’oggetto puntato: sottolineo una referenza, non una
copia, posso riassegnare l’oggetto.
std::cout << *p; //1234, dereference operator
int k = *p;
p = 5678;
++(*p);
p = nullptr;
//posso annulare il puntatore; ora p non punta a niente.
*p; // undefined behavior
Consigliabile prima di lavorare con i puntatori di mettere un assert per verificare
che non siano nulli. Vedremo meglio, ma un puntatore non nullo non è detto
che sia valido; c’è il modo di eliminare l’oggetto a cui punta il puntatore ma di
mantenere il puntatore, anche se punta al niente. Per riassumere:
• address-of operator &: dato un oggetto fornisce il suo indirizzo in memoria
• dereference operator *: dato un puntatore fornisce una referenza all’oggetto
puntato.
• structure dereference operator: -> dato un puntatore a un oggetto di
una struttura/classe qualsiasi, fornisce una referenza al dato membro
dell’oggetto puntato, tipo dato il numero complesso c = 1+2i:
Complex* p = &c;
p->r; //restituisce 1.

Abbiamo già visto un puntatore: this, che ha senso solo in un metodo, punta
all’oggetto su cui è stato chiamato quel metodo, l’avevamo visto quando abbiamo
definito l’operatore += nella classe Complex.

Nested class e type alias


Una classe può essere definita dentro la definizione di un’altra classe, sia nella
parte pubblica che privata e la classe che definisco dentro può accedere ai membri
privati della classe di cui fa parte, ma non viceversa. Volendo posso dare un
altro nome a un tipo primitivo per rendere il codice più significativo, se non
voglio creare una classe. Ci sono due modi a livello sintattico per farlo: using
Length = double;
typedef double Length; //equivalent, old alternative
Length len = 1.; //len is of type double.
Un alias type non introduce un nuovo tipo, crea un sinonimo per un tipo già

30
esistente.

Structured binding (C++ 17)


Una structured binding declaration permette di spacchettare una struttura nei
suoi membri e assegnare i valori dei suoi membri alle variabili singole. Mecca-
nismo per alleggerire la pesantezza sintattica, posso farlo sia by value sia con
una referenza:
struct Point {
double x;
double y;
};
Point p{1.,2.};
auto [a, b] = p;
std::cout << a << ’ ’ << b; //print 1 2
Queste cosine servono per capire meglio delle componenti della standard library:
vediamo ora i container e gli algoritmi.

Container of objects
Fino ad ora abbiamo visto programmi che lavoravano con oggetti singoli, ma con
programmi più complessi spesso ho bisogno di contenitori di oggetti: abbiamo
già visto le stringhe, che sono collettori di caratteri. Un container è un oggetto
che contiene altri oggetti. La standard library del c++ ne fornisce alcuni, e sono
tutti implementati come template. Ognuno di questi container ha caratteristiche
diverse, ma ci sono dei tratti comuni che poi vengono sfruttati dagli algoritmi.

Vectors
std::vector<T>. È un contenitore dinamico omogeneo di oggetti: quindi è un
contenitore che contiene oggetti tutti dello stesso tipo. Dinamico vuol dire che
la sua dimensione può variare al runtime. È il contenitore che dobbiamo usare di
default. La disposizione degli elementi è contigua in memoria: se metto dentro
dieci interi, sono messi tutti uno accanto all’altro. Questo vedremo che porta
diversi vantaggi.Notiamo che c’è differenza fra () e {}:
#include <vector>

std::vector<int> a; //empty vector of ints


std::vector<int> b{2}; //one element, initialized to 2
std::vector<int> c(2); //two elements (!), value-initialized (0
for int)
std::vector<int> d{2,1}; //two elements, initialized to 2 and 1
std::vector<int> e(2,1); //two elements, both initialized to 1
auto f = b; //il vettore a sua volta è un oggetto, se
faccio una copia copio tutti gli elementi, f e b sono due oggetti distinti.

31
f == b; //true
Come faccio a sapere quanti elementi ci sono nel vettore? Per questo c’è il
metodo size. Per vedere se il vettore è vuoto c’è il metodo empty, che restituisce
vero o falso, invece l’operatore [] mi dà una referenza all’elemento i-esimo, che
posso modificare. Da ricordare che si comincia a contare da 0, gli elementi
vanno da 0 alla dimensione-1:
std::vector<int> vec = {1,2,3};
assert(!vec.empty());
std::cout << vec.size(); //print 3
vec[1] = 5; //vec is now {1,5,3}
Per aggiungere elementi alla fine del vettore c’è il metodo push back, un
metodo per aggiungere in fondo al vettore: vec.push back(2) //vec is {1,5,3,2}.

03/01
Iterators and ranges
Un iteratore è un oggetto che indica una posizione all’interno di un range. Per
dire cos’è un range si apre un mondo, diciamo che è un container, tipo vector. Un
range è identificato da una coppia di iteratori che puntano uno all’inizio del range

e uno oltre la fine del range: intervallo semi-aperto.


Il fatto di avere un range semi-aperto porta diversi vantaggi, in primis è facile
rappresentare un range vuoto: se first == last. Se ho un vettore, come faccio
a passare alla coppia di iteratori che rappresentano quel range? Tipicamente i
container hanno due metodi, begin e end, che restituiscono proprio gli iteratori.
std::vector<int> v {· · ·};
auto first = v.begin(); //std::vector<int>::iterator
auto last = v.end(); //std::vector<int>::iterator Posso fare
delle operazioni sugli iteratori, operazioni che sono ispirate a quelle che faccio
con i puntatori, perché di fatto gli iteratori puntano a qualcosa. Un oggetto per
essere definito iteratore deve supportare delle operazioni che sono il minimo sin-
dacale: l’asterisco davanti all’iteratore *it restituisce una referenza all’oggetto
puntato da it. Visto che posso costruire i miei iteratori, che molto spesso
sono una classe, in questo caso faccio l’overload dell’operatore *. Se ho auto
it = v.end() non posso fare *it perché è un iteratore che punta fuori dal
range. Posso fare anche it->member per accedere a un membro(dato o fun-
zione) dell’elemento puntato da it. Un’altra operazione fondamentale è ++it:
avanzo di uno, l’iteratore mi porta all’elemento successivo. Per vector posso
andare indietro con –, ma ci sono alcune strutture dati che permettono solo di
andare avanti. L’ultima operazione fondamentale è it1 == it2 o it1 != it2
che dice se due iteratori puntano allo stesso elemento oppure no. Oltre a queste
ci sono altre operazioni che possono essere supportate o no, dipende dal range.
(Vector ne supporta molte).

32
Add elements of a vector
Dato un vettore std::vector<int> c { · · · }; vediamo i vari modi in cui
possiamo sommare tutti gli elementi di un vettore, più in generale su un con-
tainer.
• auto sum = 0;
for (int i = 0, n = c.size(); i != n; ++i) {
auto const& v = c[i];
sum += v; }
//notiamo che non c’è i != c.size() perché darebbe un warning, perché
size restituisce un unsigned, non un intero. Avevano deciso di farlo cosı̀
perché avevano pensato che visto che size non può essere negativo è meglio
dichiararlo unsigned che int, per questo dichiariamo una variabile aggiun-
tiva, per evitare il warning. Inoltre il loop è fatto con l’operatore != e
non con < perché oltre al fatto che cosı̀ è più preciso non tutti i container
supportano l’operazione di minore, invece quella di diverso ”!=” tutti.
• auto sum = 0;
for (auto it = c.begin(), end = c.end(); it != end; ++it) {
auto const& v = *it;
sum += v;
} //stesso loop di prima con gli iteratori
• auto sum = 0;
for (auto const& v : c) {
sum += v;
} //la variabile v scorre tutti gli elementi del range che sta a destra dei :,
utile per far capire che stiamo scorrendo tutti gli elementi.
• auto sum = std::accumulate(c.begin(), c.end(), 0);//algoritmo, la
soluzione migliore.

Vectors pt. 2
Altre due operazioni sui vettori: eliminazione di un elemento e inserimento di
un oggetto che non sia l’ultimo. Per eliminare un elemento c’è il metodo erase,
a cui devo passare l’iteratore dell’elemento che voglio eliminare, ovviamente non
posso passargli l’iteratore end. Ad esempio, per eliminare l’elemento centrale:
auto it = v.begin() + v.size() / 2;
v.erase(it);
Posso anche passare 2 iteratori e cancella tutti gli elementi tra i due iteratori,
tipo per cancellare metà di un vettore:
auto it = v.begin() + v.size() / 2;
v.erase(it, v.end()); In generale, se elimino un elemento, l’iteratore che
punta a quell’elemento non è più validi e neanche quelli che puntano dopo
potrebbero essere non validi. Per inserire un elemento in un posto che non
sia la fine uso il metodo insert, che inserisce un elemento prima della posizione

33
indicata dall’iteratore. Dato un iteratore it a un elemento ho v.insert(it,18):
inserisce nella posizione indicata da it il numero 18. Ogni volta che faccio un
inserimento tutti gli iteratori che puntano ad elementi dopo l’iteratore it sono
invalidati.

Arrays
Un altro container molto simile a vector. La grossa differenza è che array non è
dinamico, la sua dimensione è fissa, non modificabile. Container di N elementi
di tipo T. Quell’N è il classico esempio di non type template parameter. Il
layout è contiguo in memoria.
#include <array>
std::array<int,2> a; // 2 ints, uninitialized std::array<int,2>
b{1,2}; //2 ints, initialized to 1 and 2
std::array<int,2> c{}; //2 ints, value-initialized (0 for int)
std::array<int,2> d{1}; // 2 ints, initialized to 1 and 0
Anche qui c’è il metodo size, l’operatore [] dà l’accesso all’elemento i-esimo e altri
metodi sono begin, end, empty, front e back. Posso fare dei template fissando
uno dei due (o entrambi) dei parametri (tipo degli oggetti e dimensione) degli
array. Rispetto a vector, array è più efficiente in termini di spazio e se so che
i miei dati sono sempre a dimensione fissa è meglio esprimerli con un array, se
metto un vector chi legge il codice si immagina che serva dinamicità.

03/07
Algorithms
Gli algoritmi sono funzioni generiche che operano su un range di oggetti (range
definito da una coppia di iteratori). Sono function templates. Un esempio è
sommare tutti gli elementi dentro un container cont:
auto sum = std::accumulate(cont.begin(), cont.end(), 0);
Vediamo dopo che l’algoritmo accumulate si può usare in modo più sofisticato.
Un altro esempio è se vogliamo trovare nel nostro container cont un elemento
equivalente a val. Senza algoritmo:
auto it = cont.begin();
auto const end = cont.end();
for (; it != end; ++it) {
if (*it == val) {
break;}}
Con algoritmo:
auto it = std::find(cont.begin(), cont.end(), val); Cosı̀ se non l’ho
trovato it sarà uguale a end e posso verificarlo con un if dopo. Notare che
i primi due argomenti di un algoritmo in genere identificano il range su cui
l’algoritmo deve operare. Una domanda che sorge ”spontanea” è: perché gli
passo due iteratori e non un container? Il fatto è che cosı̀ sono più flessibile

34
sul range su cui voglio operare, non devo per forza prendere in considerazione
tutto il container se non mi serve. Si può definire un concept come un insieme
di requisiti che deve soddisfare un tipo al compile time, gli algoritmi sono scritti
in termini di concetti. I concetti impongono dei vincoli agli oggetti che possono
essere parametri di template. Ad esempio, supponiamo di voler scrivere un
algoritmo che avanza di uno un iteratore.
template<class T>
concept Incrementable = requires(T t) { ++t; };
template<Incrementable T>
//”guarda il metodo advance si può istanziare solo con un tipo che soddisfa
il concetto Incrementable” (lo standard fornisce alcuni concetti, altri li posso
definire io)
auto advance(T& t) { ++t; }
int i{};
advance(i); //ok, int is a model of Incrementable
struct S {};
S s;
advance(s); //error, S is not a model of Incrementable.
A titolo informativo, gli iteratori non sono tutti uguali. Ci sono alcune oper-
azioni che tutti devono supportare, poi ci sono anche delle operazioni in più.
Quindi ci sono alcuni algoritmi che richiedono certi tipi di iteratori con delle
operazioni specifiche. Vediamo alcuni esempi di algoritmi (consultare cpp ref-
erence):

• all of o any of o none of vogliamo vedere se tutti, qualcuno o nessuno


degli elementi di un vettore hanno un certo valore/soddisfano un certo
predicato.
• for each applica una funzione agli elementi.

• fill abbiamo un range e lo riempiamo con un certo valore.


• generate ogni elemento viene inizializzato da un generatore, tipo di nu-
meri casuali.
• sort ordina gli elementi in ordine crescente

• ...
Più usiamo gli algoritmi, meglio è, sono più espressivi di un ciclo for/while,
capiamo immediatamente cosa fanno, sono efficienti e indicano la complessità
computazionale, cioè quante operazioni vengono fatte. Utili anche per il par-
allelismo, cioè il modo di distribuire codice in modo che se abbiamo tipo 8
processori, il lavoro viene diviso, non fa tutto un unico processore (se devo or-
dinare 1 milione di numeri, ognuno ne ordina un tot e poi metto tutto insieme).
C++ 17 ha introdotto la possibilità di abilitare il parallelismo al posto che fare
tutto in maniera sequenziale.

35
Computational complexity
Su cppreference vedo che l’algoritmo sort ha una complessità di N log N , cioè
per riordinare N numeri fa N log N confronti. La complessità computazionale è
una misura del costo, in tempo o in memoria nel girare l’algoritmo. In genere
sono di interesse il caso medio e il caso peggiore. La complessità è una funzione
f dell’input size n, mi interessa il comportamento asintotico, si usa la notazione
degli O grandi. Quando facciamo push back è costante, find è lineare.

03/08
Customizable algorithms
Abbiamo visto alcuni algoritmi che terminavano con il suffisso if. Significa
che possiamo configurarli per fare delle azioni particolari che dipendono dalla
funzione che gli passiamo (in realtà non sono quelli che terminano con if, era
un esempio). Le funzioni sono state una delle prime forme di astrazione che ab-
biamo visto. Più funzioni possono avere lo stesso nome, questo va sotto il nome
di overloading. Una funzione che restituisce un booleano si chiama predicato.
Vediamo come gli algoritmi possono essere adattati nel loro comportamento
quando gli passiamo una funzione. Nell’algoritmo find if, rispetto all’algoritmo
find, gli passo un predicato e non un valore come nell’algoritmo find:
template <class Iterator, class Predicate>
Iterator find if(Iterator first, Iterator last, Predicate pred)
{
for (; first != last; ++first)
if (pred(*first)) //applico il predicato
break;
return first;
}
bool lt42(int n) { return n < 42; }
//predicato che è vero se il numero è minore di 42
auto it = find if(v.begin(), v.end(), lt42);
Alcuni algoritmi possono essere personalizzati passandogli una funzione.

Function objects
Oggetti che si comportano come funzioni. Il c++ offre un meccanismo per
chiamare qualcosa che sia chiamabile come una funzione, anche se non è nec-
essariamente una funzione. Si può fare l’overload dell’operatore ”chiamata di
funzione”, (), come si vede nell’esempio 2.

36
auto lt42(int n) struct LessThan42 {
{ auto operator()(int n) const
return n < 42; {
} return n < 42;
auto b = lt42(32); // true } };
vector v{61,32,51}; auto lt42 = LessThan42{};
auto it = find if( auto b = lt42(32); // true
begin(v), end(v), vector v{61,32,51};
lt42 auto it = find if(
); //*it == 32 begin(v), end(v),
lt42); oppure LessThan42{}); per
costruire un oggetto della struct al
volo, cosı̀ non devo costruire prima
un oggetto di quella classe, restituisce
*it == 32

Perché devo fare questo sforzo? Un function object, essendo l’istanza di una
classe, può avere uno stato, posso avere una classe con i suoi metodi, i suoi
costruttori e tra gli operatori anche (). Possiamo fare una generalizzazione della
classe precedente dove il numero da confrontare è un parametro:
class LessThan {
int m ;
public:
explicit LessThan(int m) : m {m} {}
auto operator()(int n) const {
return n < m ;
}
};

LessThan lt42{42};
auto b1 = lt42(32); // true
LessThan lt24{24};
auto b2 = lt24(32); //false
vector v{61,32,51};
auto i1 = find if(..., lt42); //*i1 == 32
auto i2 = find if(..., lt24); // i2 == end, non c’è nessuno che sia
minore di 24. Anche qui posso evitare di dare un nome, posso creare un oggetto
al volo con LessThan{42} e passarlo alla funzione.

Lambda expression
La funzionalità di avere dei function objects è stata considerata cosı̀ utile che
è stata inventata una sintassi apposita per costruire degli oggetti al volo da
passare agli algoritmi. L’uso degli algoritmi ha richiesto di scrivere molte classi
con l’operatore (). Le lambda sono una semplificazione per creare degli oggetti

37
funzioni anonimi, senza nome. Oltre agli algoritmi vengono usate anche in al-
tri contesti. Vediamo come si possono costruire degli oggetti funzioni in forma
semplificata (in alto senza stato, in basso con stato). Le due [] annunciano
che sta per cominciare un’espressione che riguarda la produzione di un oggetto
funzione. Possiamo immaginare le lambda come una notazione semplificata per
definire delle funzioni oggetto (una classe con l’overload dell’operatore ( )) e poi
subito dopo creare un oggetto di quella function object. Al posto di fare:
struct LessThan42 {
auto operator()(int n)
{
return n < 42;
}};
find if(..., LessThan42{});
Posso usare una lambda:
find if(..., [](int n) {return n < 42;});
Notiamo che una lambda in generale si può definire cosı̀: [ capture clause ]
(parameters) -> return-type { definition of method } (in generale pos-
siamo omettere la parte -> return-type perché il compilatore lo deduce). Le
captures sono una lista separata da virgole di zero o più parametri, che definisce
le variabili che si trovano fuori a cui la lambda può accedere. Se mettiamo
[&] tutte le variabili esterne sono prese by reference, cosı̀ [=] tutte le variabili
esterne vengono copiate. Non c’è modo di catturare by const&, quindi se cat-
turiamo le variabili by reference, sono modificabili. Se invece le passiamo by
value di base non sono modificabili(vedi const and mutable per ulteriori det-
tagli). Se non mettiamo nulla la lambda può accedere solo alle sue variabili
locali. Altro esempio più complicato, al posto di:
class LessThan {
int m ;
public:
explicit LessThan(int m) : m {m} {}
auto operator()(int n) const
{
return n < m ;
}};
find if(..., LessThan{m});
Posso mettere questa lambda:
int m = · · ·;
find if(..., [=](int n) {return n < m;});
posso dichiarare anche m dentro le []: [int m=...] La lambda produce un
oggetto anonimo che viene chiamato closure. Questo oggetto è un function ob-
ject. Data una function object con dei dati membri privati, questi nella lambda
sono catturati dalla capture. Ogni lambda è di un tipo diverso. Facciamo un
altro esempio non legato agli algoritmi:
int main() {
int a = 3;
auto l = [&] {++a;};

38
l();
return a; mi ritorna 4.
Posso applicare direttamente l’operatore di funzione alla lambda:
int main() {
int a = 3;
[&] {++a;}();
return a;
Quando abbiamo fatto le funzioni avevamo detto di fare attenzione a non ri-
tornare by reference una referenza che fa riferimento a una variabile locale della
funzione, perché nel momento in cui la funzione finisce, quell’oggetto non esiste
più, dobbiamo stare attenti a non incorrere in errori simili quando catturo by
reference nella lambda.

03/14
Dynamic memory allocation
Quando si usano questi strumenti è facile incorrere in situazioni in cui ci si fa
del male. Ci sono situazioni di dinamicità in cui non si può o non è conveniente
costruire oggetti sullo stack, dove sono distrutti alla fine della funzione che li ha
creati. Questo perché è il compilatore che muove la freccina stack pointer, quindi
se qualcosa non è noto al compile time, non può andare, almeno completamente,
sullo stack. Ci sono situazioni in cui ho bisogno di costruire un oggetto/array di
oggetti sul free store o heap. Per costruire gli oggetti su quest’area di memoria
si usa l’operatore speciale new o new [] se voglio costruire un array di oggetti.
Quest’operatore fa due cose: alloca un’area di memoria sufficientemente grande
da tenere il mio oggetto e su quell’area di memoria fa girare il costruttore. Il
problema nasce perché nel momento in cui costruisco sullo heap non è più il
compilatore che fa pulizia per me quando arrivo in fondo allo scope, ma devo
essere io che esplicitamente quando non ne ho più bisogno devo chiamare un op-
eratore speciale che fa pulizia, che si chiama delete o delete []. L’operatore
delete fa le cose simmetriche rispetto all’operatore new: prima gira una fun-
zione speciale della classe che si chiama distruttore(ora lo vediamo) e dealloca
memoria, restituendola al sistema. L’operatore new restituisce qualcosa che mi
permette di usare quell’oggetto, un puntatore all’oggetto appena creato sullo
heap. Quindi io ho un puntatore e posso fare tutte le mie cose con il puntatore.
Quando faccio il delete non vado a modificare il valore dell’oggetto, è un modo
per dire al sistema ”guarda questo valore non mi serve più”. Dopo che è stata
fatta la delete, l’unica operazione che si può fare è l’assegnamento. Se il punta-
tore è nullo, nullptr, posso comunque fare il delete e non succede niente. Dopo
che ho fatto delete, il puntatore punta dove puntava prima, solo che ora non c’è
più niente. Quando creo un oggetto all’interno di una funzione e poi voglio che
alla fine della funzione l’oggetto sopravviva. L’unico modo per fare ciò è creare
l’oggetto sullo heap e restituire il puntatore.

39
Regression* create() //funzione che vuole costruire un oggetto
di tipo regression e restituisce il puntatore a questo oggetto
{
auto rc = new Regression{};
rc->add(· · ·);
· · ·
return rc;
}
auto use()
{
auto ru = create(); //inizializzo ru usando la funzione cre-
ate, quindi ru è di tipo Regression*. Alla fine viene restituito il puntatore,
quindi il valore del puntatore che punta all’oggetto rc di tipo Regression viene
copiato fuori dalla funzione
ru->fit();
· · ·
delete ru;
}
La vita dell’oggetto allocato dinamicamente è gestita completamente dallo svilup-
patore. Vediamo le sequenze di oggetti contigui. std::array e std::vector
sono forniti dalla libreria, quindi sono implementati su qualcosa di più fonda-
mentale, che è offerto dal linguaggio. Questa cosa sono gli array nativi di oggetti.
Sia std::array che std::vector sono implementati sopra questa cosa. Per gli array
nativi, se voglio fare una copia tipo auto b = a b non è un array come a, è un
puntatore al primo elemento di a. Per allocare dinamicamente un array:

auto fun()
{
int a[3] {12, 34, 56};
int* p = new int[3] {12, 34,
56};
· · ·
delete [] p;
}

Situazione in memoria prima di


delete.

Notiamo che anche se p è un puntatore al primo elemento, facendo p[ith ]

40
abbiamo accesso all’i-esimo elemento. P non si porta dietro informazioni sul
fatto che che è un array, è un puntatore al primo elemento. Dopo la delete, il
valore di p resta inalterato. Se volessi implementare vector ho la possibilità di
allocare spazio in maniera dinamica, di settare la dimensione al runtime:
auto fun()
{
int n;
std::cin >> n;
auto p = new int[n];
· · ·
delete [] p;
}
Gestire correttamente degli array raw è difficile. Per passare un array a una
funzione gli devo passare un puntatore e la size. Il messaggio è meglio usare
oggetti di più alto livello, come std::array o std::vector.

Weaknesses of T*
Quando ci troviamo in mano un T* dovremmo porci una domanda: cosa posso
fare con questo puntatore? Cos’è ammesso fare e cosa devo fare? Per T* inten-
diamo il puntatore a un tipo qualsiasi. Non so se sono responsabile della vita
dell’oggetto che sta dietro a quel puntatore. Se mi arriva un T* in una funzione
e alla fine esco dalla funzione, lo devo cancellare o no? Questa informazione non
è codificata nel tipo, mi deve arrivare da altri contesti. Se ho
Shape* s = create shape(); probabilmente devo fare delete. Oppure non so
se ho un oggetto o un array. Oppure un altro problema è se mi dimentico di
fare la delete e ho un cosiddetto memory leak, cioè una perdita di memo-
ria, perché non ho più un collegamento con la memoria e non posso più fare
la delete. In genere con Root non c’è bisogno di fare la delete se allochiamo
memoria dinamicamente, fa pulizia da solo. Se faccio delete due volte, ho un
altro problema. Un caso speciale in cui mi dimentico di fare delete è il caso delle
eccezioni. Una delle caratteristiche delle eccezioni è che se sollevo un’eccezione
in una funzione, l’effetto è uscire dalla funzione. Quindi tutto quello che è sullo
stack viene cancellato e l’istruzione delete viene persa perché il puntatore viene
cancellato, ma la memoria non viene eliminata. Oltre a questo c’è un problema
di performance: se alloco e dealloco dinamicamente le performance diminuis-
cono rispetto ad allocare sullo stack, sia in termini di spazio (memoria occupata
dal puntatore) che di tempo. Ho un problema riguardo la performance di in-
direzione: seguire il puntatore per arrivare a un oggetto è costoso. Per sapere
se il mio programma dal punto di vista dell’uso della memoria è corretto, uno
degli strumenti più semplici da usare è Address Sanitizer. Compilando il pro-
getto in questo modo non deve dare errori. Per usarlo quando facciamo g++ gli
passiamo fra le varie opzioni -fsanitize=address. Se è inevitabile allocare di-
namicamente, meglio affidarsi a container e strings o a smart pointers(vedremo
cosa sono).

41
03/15
Implementazione dynamic array
(Sezione incompleta causa esaurimento, buona fortuna). Ho bisogno di memoria
dinamica, come faccio a gestirla senza commettere errori? Vediamo come si può
accennare l’implementazione di una classe che gestisce la memoria dinamica.
Vogliamo implementare un array ma la cui dimensione è nota solo al runtime.
La memoria non è l’unica risorsa che abbiamo sul computer. Nel momento in cui
interagiamo di più con il sistema ci sono delle altre risorse, come i thread, i di-
versi flussi di esecuzione, o i file. Uno dei principi guida di c++ è quello di poter
gestire le risorse in maniera corretta. La classe dynamic array si basa sull’array
nativo, che può essere allocato dinamicamente. Introduciamo in questo esempio
il distruttore, una funzione speciale che viene chiamata quando l’oggetto va fuori
scope, quando arrivo a }. Le variabili locali cosı̀ vengono distrutte. Distruggere
significa non solo liberare l’area di memoria ma chiamare, se c’è, il distruttore.
Il distruttore è unico. Vengono distrutti tutti i dati membri e infine liberata
memoria. Anche l’ordine di distruzione, come l’ordine di costruzione dei mem-
bri, è importante. Distruggo in ordine inverso rispetto all’ordine di costruzione.
La segnatura del distruttore è: ∼ classname(). Il codice si trova su github, è
un modo per mostrare che allocare memoria dinamicamente è un casino, meglio
usare quello che viene offerto dalla standard library o da altre librerie.

Smart pointers
Sono degli oggetti che si comportano come puntatori, posso deferenziarli con l’*
o con la freccina ma si occupano anche di gestire la vita dell’oggetto puntato,
più probabile che li dovremmo usare rispetto all’argomento precedente, perché
ci sono dei casi in cui l’allocazione dinamica di memoria è obbligatoria.
template<typename Pointee>
//template perché punta a un oggetto p di tipo arbitrario class SmartPointer
{
Pointee* m p;
public:
explicit SmartPointer(Pointee* p): m p{p} {}//costruttore che prende
un puntatore all’oggetto puntato
∼SmartPointer() { delete m p; }//quando va fuori scope viene fatta la delete
Pointee* operator->() { return m p; }//restituisce il puntatore sottostante
Pointee& operator*() { return *m p; }//mi restituisce una referenza all’oggetto
puntato
};

class Regression { · · · };

{
SmartPointer<Regression> sp{new Regression{}};

42
//inizializzo l’oggetto sp come uno smart pointer a un oggetto di tipo regression
allocato dinamicamente sullo heap. Quando inizializzo un oggetto tipo new Re-
gression{} mi viene restituito un puntatore che affido a smart pointer
sp->add(· · ·);
(*sp).fit();
//mi restituisce una referenza all’oggetto puntato su cui chiamo il metodo fit }
Alla fine dello scope sp viene distrutto, quindi faccio la delete di new Regres-
sion{}. Oltre alla parte smart c’è la parte pointer, quindi voglio continuare
a usarlo come un puntatore. Anche questo è relativamente facile perché c’è
l’overload degli operatori. Ci sono due smart pointer principali nella standard
library. Quello che dovremmo usare più spesso è std::unique ptr<T>. Modella
la cosiddetta ”exclusive ownership”, ossia la proprietà esclusiva di un punta-
tore/dell’oggetto che sta dietro al puntatore. Non è copyable, ma movable. Se
ho la proprietà esclusiva di un oggetto che sta in memoria non posso condi-
viderla. Facciamo un esempio:
class Regression { · · · };
void take(std::unique ptr<Regression> q); //funzione che prende come
parametro uno unique pointer a Regression by value
std::unique ptr<Regression> p{new Regression{}};
auto p = std::make unique<Regression>(); //fa sempre uno unique pointer
come prima, ma usa questa funzione
auto r = p; //error, non-copyable
take(p); //error, non-copyable
Non copiabile perché ci deve essere un solo pointer che ha la proprietà di
quell’oggetto in memoria. Se voglio trasferire il controllo da p a r c’è un modo
apposito, uso la funzione move, che esplicita il fatto che la proprietà passa da
un oggetto all’altro:
auto r = std::move(p); //ok, movable
take(std::move(r)); //ok, movable
Quindi std::move() trasferisce la proprietà della risorsa da uno smart pointer
all’altro. Per disabilitare l’operazione di copia, il modo migliore per farlo è mar-
care le operazioni di copia con la keyword =delete. (Ovviamente questo se sto
implementando il mio personale smart pointer). Per implementare il trasferi-
mento di ownership, tipica dello unique pointer, non posso usare la copia, devo
trovare un altro modo. Il c++ 11 ha introdotto un altro tipo di referenza, rvalue
reference, identificata nel codice con &&. Rispetto alla copia, la sorgente viene
modificata. Una classe ha cinque five member functions che il compilatore
è in grado di generare se non le facciamo noi, oltre al default constructor sono
il copy constructor, il move constructor, il copy assignment operator, il move
assignment operator e il destructor. Vale la rule of zero, cioè che in genere non
bisogna implementare queste funzioni, ma se lo facciamo, dobbiamo definirle
tutte quante. Se dobbiamo definirle, consideriamo di usare le semplificazioni di
=default, in cui si dice al compilatore che mi va bene l’implementazione che fa
lui e =delete, per dire che non voglio quella funzione, che non mi va che quella
classe sia copiabile ad esempio.

43
03/21
Git
git add, git log, git commit, git diff
Git è un tool che aiuta a proteggere te stesso e gli altri da te stesso e gli al-
tri. Utile per mantenere traccia delle versioni precedenti dei file, se abbiamo
un problema si riesce sempre a vedere quale versione del codice ha causato quel
problema. Git può tenere traccia dei file presenti in una cartella, ne mantiene
la storia, permette di tornare indietro dopo aver fatto delle modifiche e per-
mette anche di far lavoro in concomitanza sullo stesso file da più persone. Si
chiama anche Version Control. Si parte con il comando su terminale git
init, che inizializza la working directory. Nel momento in cui lancio il comando
su una cartella, il terminale risponde con ”Initialized empty git repository in
/path7.git/”. Facendo ls -la vediamo che è stata creata una cartella chiamata
.git. Cosı̀ abbiamo inizializzato la repository. La repository è la cartella .git,
è qualcosa di più di una cartella. Un altro comando utilissimo è git status,
che descrive lo stato attuale della repository di lavoro. Possiamo immaginare il
nostro ambiente di sviluppo che contiene al suo interno due cartelle, la work-
ing directory e la local repository. Sono due cose separate ma intrinsecamente
collegate. Se aggiungo un file alla working directory questo non viene messo
automaticamente anche sulla local repository. Nel nostro ambiente di sviluppo
c’è anche una staging area, dove ci sono esclusivamente i file di cui Git deve
tenere traccia. Quindi nonostante la mia cartella possa essere piena di file sono
io che decido quali sono importanti, di cui voglio tenere traccia. Finché non
aggiungo file nella staging area git non tiene traccia delle modifiche che faccio.

Il comando git add filename chiede a Git di cominciare a tenere traccia del
file nella repository, è sufficiente questo per mettere il file nella staging area.
Possiamo anche aggiungere più file. Git gestisce in maniera ottimali file testu-
ali: non soltanto tiene traccia del file in sé per sé, ma è anche in grado di dirmi
se sono state aggiunte delle righe di un file. Un commit è il sistema che usa Git
per tener traccia di una serie di modifiche ai file che devono essere raccolte tutte
quante sotto la stessa ”cuffia” di modifiche. Con git commit possiamo segnare
che tutte le cose nella staging area devono essere salvate nella repository in un
punto della sua storia con un dato nome e questo ci permetterà di avere un

44
riferimento a quel dato momento della storia. Non faccio un commit per ogni
singola modifica, lo faccio quando ho fatto delle modifiche significative. Ogni
commit dovrebbe essere etichettato con un messaggio che indica i cambiamenti
fatti git commit -m "Commit message". La repository salva i commit come
un punto della storia. Per far vedere la storia, possiamo usare il comando git
log (Per una versione più compatta git log --oneline). Abbiamo un id che
rappresenta univocamente il nostro commit. Un altro comando molto impor-
tante è git diff, che permette di vedere la differenza fra lo stato attuale della
repository dell’ultimo commit e vari altri stati. Se lo lanciamo senza niente
ci fa vedere la differenza fra l’ultimo commit e tutto quello che non è ancora
stato committato. Quando gli passiamo come argomento --staged ci fa vedere
la differenza tra l’ultimo commit e gli staged changes. Se aggiungiamo come
argomento l’identificativo di un commit ci fa vedere la differenza fra l’ultimo e
il commit di cui gli diamo l’identificativo. Basta mettere solo i primi 5 caratteri
dell’id, non tutto.

Branching e .gitignore
Un altro concetto importante è il branching, che permette di tenere traccia di
linee di sviluppo parallele. Le linee di sviluppo si chiamano proprio branches.
Questo ci permette proprio di creare un punto di fermo nella nostra repository,
dove di tutte le modifiche che andremo a fare ne verrà tenuta traccia separata-
mente, senza andare ad intaccare la linea principale dello sviluppo. Si fa con
git branch. Se lanciato senza nessun argomento, ci fa l’elenco dei branch pre-
senti. L’* rappresenta il branch dove mi trovo al momento. La linea principale
di sviluppo si chiama master branch, ora si tende verso termini più inclusivi,
si sta quindi cercando di passare dal termine master al termine main. Si può
cambiare il nome del branch (esempio da master a main) con il comando git
branch -m master main. Per creare un nuovo branch usiamo il comando git
branch branch name. La storia della repository di un branch è indipendente
dalla storia della repository di altri branch. Per ”attivare” il nuovo branch
facciamo git checkout branch name. Switchando al nostro branch secondario
andando su vs code vediamo che ci porta allo stato del nostro codice in cui è
stato creato il nuovo branch. Se vogliamo vedere i commit su tutte le linee di
sviluppo e non solo su quella dove siamo in ordine cronologico facciamo git log
--all. Cerchiamo comunque di tenere il nostro sviluppo alternativo il più breve
possibile e di ricondurci al più presto possibile sulla linea di sviluppo. Il merge
è il modo che ha Git di riunificare tutte le modifiche fatte su più branch su un
unico branch. Git cerca di riunire i due rami separati in maniera separata. Per
farlo, prima vado al branch di destinazione con checkout e poi uso il comando
git merge branch name. Il più delle volte quando facciamo un merge però ab-
biamo un conflitto e Git non riesce a fare il merge in maniera automatica. Se
le due modifiche ai due branch vanno a modificare le stesse linee dello stesso
file Git va in confusione, non sa c++. Quindi ci dice di risolvere il conflict
prima. Una volta fatto il merge elimino il branch secondario con git branch
-d branch name. Se non è stato fatto il branch questo comando non ce lo fa

45
fare, allora se lo vogliamo eliminare sostituisco -d con -D. Su git si tengono solo
i file testuali, i file binari, quelli prodotti dalla compilazione. Per dire a git di
non guardare mai quel file (altrimenti ogni volta che ricompiliamo anche se non
cambiamo il codice ci dice che il file binario è stato modificato) possiamo creare
il file di testo .gitignore e al suo interno ci mettiamo una riga con il nome dei
file che vogliamo escludere. Mettendo nella lista *.out vengono ignorati tutti i
file che hanno quest’estensione. È importante fare il commit del file .gitignore,
come della configurazione clang-format.

Github
Git permette anche di salvare le modifiche fatte al codice su una remote repos-
itory, una copia della nostra repository locale. Cosı̀ abbiamo un backup del
nostro lavoro e collaborare con più utenti nello stesso file. Github è un provider
per ospitare online le repositories, permette di fare delle repository sia private
che pubbliche. Posso sia creare una repository nuova sia caricare una repository
locale con push. Quando vogliamo caricare la nostra repository su GitHub è
richiesta la chiave SSH. SSH è un sistema di crittografia presente di default su
tutti i sistemi operativi. Generando una chiave abbiamo una chiave privata,
id rsa univoca della nostra macchina cifrata che non dobbiamo dare a nessuno.
Quella pubblica, id rsa.pub, è quella che forniamo ai vari provider e si assicura
che ci sia la chiave privata. La chiave pubblica è quella che dobbiamo fornire a
GitHub per accedere alla nostra repository. Per dire a git che la nostra reposi-
tory locale deve essere anche salvata in remoto usiamo il comando git remote
add origin repository ssh url. Una volta fatto il collegamento, sulla repos-
itory remota non c’è niente. Per fare il push: git push -u origin main(origin
nome repository per convenzione, main perché ho cambiato il nome del branch
da master a main). La repository remota è l’esatta copia della repository lo-
cale, quindi posso vedere anche i vari commit. Facciamo il push ogni volta che
vogliamo caricare i nostri cambiamenti sulla repository remota. Se vogliamo
che la nostra repository locale si renda conto di una modifica che è stata fatta
in remoto usiamo git fetch. Fetch non modifica la mia cartella di lavoro, ma
solo la repository. Per dire che voglio i dati nella cartella di lavoro faccio git
pull, in cui il fetch è implicito. Come per i merge possono esserci dei conflitti
se lavoriamo su cose diverse in contemporanea. L’ultimo comando importante
è git clone seguito dal link della repository remota se voglio copiare la repos-
itory remota e salvarla come repository locale. Clonare è sempre possibile, ma
se voglio fare un push devo avere l’autorizzazione (basta andare su github e
invitare collaboratori).

46
03/28
std::shared ptr<T>
Smart pointer, oggetti concettualmente semplici, gli assegno una parte di memo-
ria e poi me ne dimentico, chiamano loro il distruttore. Oltre allo unique pointer,
che modella la responsibilità esclusiva di un’area di memoria. Un altro smart
pointer è lo share pointer, che modella una responsibilità condivisa. L’idea, per
paragonarlo alla vita reale, è ”l’ultimo che esce dalla stanza spegne la luce”.
Shared ownership, c’è un contatore da qualche parte che conta quanti oggetti
sono responsabili di quell’area di memoria. A differenza di unique pointer, è
copyable e movable, dove copyable rappresenta l’incremento di uno del conta-
tore. Vediamo come si usano gli smart pointer. Intanto da notare come da
unique pointer si può sempre passare a shared pointer, ma il contrario non si
può fare, una volta che la responsibilità è condivisa, rimane condivisa. Quello
che si fa di solito quindi è creare uno unique pointer e eventualmente dopo
passare a uno shared pointer. Posso chiedere allo smart pointer qual è il suo
puntatore sottostante tramite la funzione smart ptr<T>::get(), che ritorna
un puntatore T* che è non owning. In realtà c’è anche la possibilità, tramite la
funzione di release, di avere un puntatore owning. Posso anche gestire gli arrays.
Ho in mano uno smart pointer e devo scrivere una funzione che prende l’oggetto
che è di proprietà dello smart pointer. La linea guida è che alla funzione non
passo lo smart pointer, a meno che non voglia trasferire la responsibilità. Posso
passare lo smart pointer by reference se applico un metodo proprio dello smart
pointer, ma altrimenti è sempre meglio passare l’oggetto, cosı̀:
void fit(Regression* t) { if (t) t->fit(); }//primo modo
void fit(Regression t) { t.fit(); }//secondo modo.
Simmetricamente se ho una funzione che restituisce una risorsa allocata dinami-
camente, la raccomandazione è restituire uno unique pointer. Ribadiamo che
questi concetti valgono per ogni risorsa, non solo per la memoria. Shared e
unique pointer sono fatti di default per gestire la memoria, ma possono essere
”resource handlers” più generali, tramite il costum deleter: se non devo am-
ministrare della memoria, ma un altro tipo di risorsa, alla fine non devo fare
il delete, ma qualcos altro, e lo posso quindi personalizzare. Ad esempio, se
voglio aprire un file chiamo la funzione fopen che mi restituisce un puntatore a
quella struttura. Anche se è un puntatore, non devo chiamare delete, lo voglio
chiudere:
FILE* f = std::fopen(· · ·);
· · ·
std::fclose(f);
Con close abbiamo gli stessi problemi di quando allochiamo memoria dinami-
camente, posso dimenticarmi di fare close, o la faccio due volte, o magari
non sono io il proprietario. Quindi anche qui sarebbe utile utilizzare uno
smart pointer equivalente e si può effettivamente fare, tramite il costum deleter.
std::shared ptr<FILE> file{
std::fopen(· · ·), // pointer

47
(FILE* f) { std::fclose(f); } // deleter
};

Containers pt. 2
I container che abbiamo visto, vector e array, sono di tipo sequence, cioè decido
io dove stanno i vari elementi. Nei contenitori associativi invece diciamo che
vogliamo inserire un elemento e poi è il contenitore che secondo certe regole de-
cide dove metterlo. Gli associativi si dividono in ordinati, in cui hanno un certo
ordine, tipo crescente, com’è il container set(anche se in realtà è meglio metterli
in un vector disordinati e poi chiamare sort). Invece megli unordered/hashed
la posizione di un elemento è determinata dall’applicazione di una funzione.
Vector è implementato tipicamente con 3 puntatori: il primo punta all’inizio, il
secondo punta uno dopo la fine e l’ultimo alla fine dell’area di memoria allocata,
alla fine della capacity, cosı̀ ogni volta che faccio push back non devo riallocare
spazio: ogni volta viene allocata più memoria del necessario secondo una dispo-
sizione geometrica, di solito di un fattore 2. Negli associative ordered containers
le operazioni di inserimento/ricerca/rimozione di un elemento ha complessità
computazionale logaritmica, quando ho molti valori questo conta. Con il con-
tainer map possiamo fare un programma in cui gli passo un file testuale e mi
dice quante volte compare una certa parola (ad esempio gli passo la divina com-
media e conto quante volte viene ripetuta la parola Beatrice), il codice si trova
su Github.

Non-local variables
Se ne scoraggia l’utilizzo. In tutti gli esercizi che abbiamo visto, le variabili
vengono tutte definite all’interno di un blocco, di una funzione. Però esiste
la possibilità di definire delle variabili fuori dal blocco di una funzione. Le
variabili visibili da ogni funzione nel file si chiamano variabili globali. Le
variabili locali stanno nel blocco allocato dallo stack nella funzione, le variabili
globali stanno nella parte della memoria chiamata static data (vedi figura
memoria all’inizio), vivono per tutta la durata del programma. Il costruttore
delle variabili locali viene chiamato prima del main. Quindi l’inizializzazione
delle variabili globali viene fatta prima del main. Le variabili globali sono tutte
inizializzate a zero, mai indeterminate. La raccomandazione è di non usarle,
perché quando guardiamo un pezzo di codice ci piacerebbe che la comprensione
del codice sia tutta dove stiamo guardando. Se abbiamo delle variabili globali,
è difficile capire il codice. Inoltre ci sono dei problemi pratici, perché l’ordine
di inizializzazione è deterministico solo all’interno della stessa translation unit,
se abbiamo variabili globali in più translation unit non è definito quale viene
inizializzata prima e se le variabili dipendono l’una dall’altra iniziano i problemi.
Per le costanti ci sa che siano delle variabili globali, se dobbiamo definire π.
constexpr double pi = 3.14159265358979323846;
La keyword constexpr implica const e assicura che al compile time la variabile

48
sia inizializzata con quel valore, non che venga prima messa a 0 e poi al runtime
inizializzata con il valore giusto. Importante perché il linguaggio sta spingendo
molto nella direzione in cui si fa più computazione possibile al compile time.

Classic static data members and members functions


In alcune situazioni è utile avere dei dati membri che non sono associati ad ogni
singolo oggetto della classe che creo, ma associati alla classe: tutti gli oggetti
della classe hanno quel dato. Se voglio contare quanti oggetti della classe ho
costruito, il contatore non fa parte degli oggetti, ma è legato alla classe e esiste
indipendentemente dagli oggetti. Anche questi dati membri hanno vengono
messi nella parte di memoria static data e i dati membri di questo tipo sono
static. La regola generale è che la sua definizione dovrebbe venire fuori dalla
classe (ovviamente la dichiarazione dentro), a parte un certo numero di casi in
aumento. Per accedere a uno static data member posso usare l’operazione di
scope sulla classe. Se ho un oggetto, posso usare comunque l’operatore dot
come al solito. Ad esempio:
struct system clock { static constexpr bool is steady = true
auto f(){
system clock::is steady;}
Come i data members, ci sono anche le funzioni membro che possono essere
static. Li posso invocare indipendentemente dal fatto che ho invocato un oggetto
di quella classe.

03/29
Polymorphism
Il poliformismo è fornire una stessa interfaccia a entità di tipo diverso. Abbiamo
già visto il poliformismo statico con l’uso dei template e dei concetti. Vedremo
il poliformismo dinamico, che è un sinonimo di programmazione Object Ori-
ented, basato sulle funzione virtuali e sul meccanismo di ereditarietà. Vediamo
il concetto di inheritance:
struct Base{
int i;
void f();
Base(int i): i(i){}
Magari data una classe mi piacerebbe aggiungere qualcosa, ma magari non sono
l’autore di base. Quindi posso ereditare da base, in questo modo:
Struct Derived:Base //significa che eredito da base
{...} //ci aggiungo quello che mi serve. Il
vantaggio è che se ho un oggetto della classe derivata posso accedere a tutti i
membri della classe base. (Poi vediamo quali sono le regole di accessibilità della
classe derivata rispetto alla classe base). Quando scrivo il costruttore della classe
base, devo costruire anche la parte base, quindi inizializzo tutti i membri della

49
classe base. Posso ereditare quello che c’è sulla classe base come fosse parte
della classe derivata. Posso avere una gerarchia: tipo Derived2 che deriva da
Derived che deriva da Based. Il polimorfismo dinamico era molto di moda negli
anni ’90, si pensava che un vantaggio notevole di questo stile di programmazione
era che si potesse riusare semplicemente il codice. In realtà non è proprio cosı̀,
ora il grosso dell’evoluzione sta avvenendo più sulla programmazione generica.
Root si basa su questo modello. Il problema tipico che viene risolto con questo
paradigma è quello dei sistemi grafici, dove abbiamo delle figure geometriche che
possono avere delle forme diverse e non posso costruire un vettore di shapes,
perché vector è un container omogeneo, quindi gli oggetti devono essere tutti
dello stesso tipo. Oppure voglio traslare la figura di una certa quantità. Per
fare qualcosa di questo tipo, uso il polimorfismo dinamico:
struct Shape { //base class
};

struct Circle : Shape { //derived class


Point c;
int r;
Point where() const;
};

struct Rectangle : Shape {


Point ul;
Point lr;
Point where() const;
};
Shape create shape() //funzione che mi restituisce un cerchio
o un rettangolo. auto s = create shape();
s.where(); //qui ho un errore perché Shape non ha
il metodo where. Per fare in modo di invocare il metodo where su shape facendo
in modo che poi a questo messaggio risponda la vera shape che c’è dietro?
Questo è il ruolo delle funzioni virtuali:
struct Shape { //abstract base class
virtual Point where() const = 0;};//pure virtual function

struct Circle : Shape { Point c;


int r;
Point where() const override;
};

struct Rectangle : Shape {


Point ul;
Point lr;
Point where() const override;
};
override è per dire che il metodo where della classe derivata fa loverride della

50
funzione virtuale messa in shape. Il significato è che io chiamo where su un
oggetto tipo shape e risponde l’oggetto specifico che ci sta dietro. Shape è
l’interfaccia, il punto di accesso, ma a rispondere è o il cerchio o il rettangolo.
Mettendo virtual la chiamata viene reindirizzata alla funzione vera. Quando ag-
giungo una funzione virtuale pura la based class diventa Abstract. Non posso
creare un oggetto di tipo abstract based class, è semplicemente un’interfaccia,
quindi se faccio Shape create shape(); ho un errore, ma posso fare questo:
Shape* create shape(); posso avere in mano un puntatore a shape
che verrà inizializzato come puntatore a circle o rectangle
auto s = create shape();
s-¿where();
Per lavorare con il polimorfismo dinamico di fatto sono obbligata ad allocare
dinamicamente. Quindi spetta a me fare la delete e anche il distruttore deve es-
sere virtuale e devo arrivare ad eseguire i rispettivi distruttori. Il fatto che c’è un
distruttore implica, per la five rule, che devo ragionare anche sulla copiabilità
e implementare tutte le special member function, anche se magari dicendogli
=default. Siccome ho dei raw pointer, la prima cosa che devo fare è assegnarlo
a uno smart pointer, che automaticamente fa la delete, anche se devo mantenere
comunque i distruttori. Se non metto l’override comunque funziona il meccan-
ismo di ridirezione, ma override previene alcuni errori, comunque è un dettaglio.
struct Shape {
virtual ∼Shape(); //no ’= 0’ here
virtual Point where() const = 0;
};

struct Circle : Shape {


Point c;
int r;
∼Circle();
Point where() const override;
};

struct Rectangle : Shape {


Point ul;
Point lr;
∼Rectangle();
Point where() const override;
};
std::unique ptr<Shape> create shape();//use a smart pointer
auto s = create shape();
s->where(); //non devo fare delete s perché sto us-
ando lo smart pointer.
Le funzioni di cui si fa override devono avere la stessa signature, cioè la stessa
dichiarazione. Se non sono identiche ci potrebbero essere delle situazioni in cui
invece di fare override sto definendo un’altra funzione. Le funzioni virtuali pure
non possono essere definite, è solo un’interfaccia. Non posso creare oggetti

51
dell’abstract base class, cioè una classe dove c’è una funzione virtuale pure. Il
polimorfismo dinamico lavora con i puntatori e anche by reference. Derived non
è una classe astratta, quindi la posso tranquillamente allocare sullo stack. Sono
obbligato a lavorare by pointer anche se funziona pure by reference perché se
voglio restituire un oggetto da una funzione o voglio mettere qualcosa in un
vettore, non posso restituire una referenza o mettere nel vettore delle referenze.
Quindi sono indotto a lavorare con i puntatori.
struct Base { virtual void f(int) = 0; };
struct Derived : Base { void f(int) override {} };
Base b; // error
Base* b1 = new Derived; //owning pointer, remember to delete
b1->f(); //calls Derived::f
Derived d; // ok
Base* b2 = &d //ok, non-owning pointer, don’t delete
b2->f(); //calls Derived::f
Base& b3 = d; // ok
b3.f(); //calls Derived::f
Supponiamo di voler fare un vettore di figure geometriche e chiamare una fun-
zione su ogni elemento del vettore. Shape ha una pure virtual function, quindi
è un abstract base class, non posso avere un oggetto di tipo shape, quindi non
posso neanche metterli nel vettore. Posso però mettere dei puntatori a shape.
Vediamo come implementarlo:
#include <algorithm>
#include <iostream>
#include <random>
#include <vector>
#include <memory>

struct Shape {
virtual ∼Shape()=default;
virtual void what() const=0; };

struct Circle : Shape {


∼Circle() =default;
void what() const override { std::cout << "Circle"; } };

struct Rectangle : Shape {


∼Rectangle() =default;
void what() const override { std::cout << "Rectangle"; } };

std::default random engine eng;


std::uniform int distribution<int> dist(0, 1);

std::unique ptr<Shape> create shape() { //se mi dà 0 restituisce un pun-


tatore a un cerchio, altrimenti a un rettangolo
if (dist(eng) == 0 {return std::make unique<Circle>();}

52
else {return std::make unique<Rectangle>();} }

int main() {
std::vector<std::unique ptr<Shape>> shapes(10);
std::generate n(shapes.begin(), shapes.size(), create shape);//eseguo
create shape n volte e il risultato che mi dai lo metti dentro il range shapes
for (auto const& s : shapes) { //se facciamo una copia dà errore, perché
è uno unique pointer
s->what();
std::cout<<endl;}
}
Ricordarsi il distruttore virtuale. Come il costruttore quando facevamo l’inheritance
chiamava il costruttore della classe base. In maniera simmetrica il distruttore di
derived deve chiamare il distruttore della classe base. Quindi il distruttore della
classe base non può essere pure virtual, deve esistere, non può essere uguale a 0.
Il processo è questo: si entra inizialmente dal distruttore della classe base e si
va al distruttore della classe derivata, ma poi internamente il distruttore della
classe derivata deve chiamare il distruttore della classe base, perché la classe
base fa parte della classe derivata.

04/04
Mixing interface and implementation
La scorsa volta abbiamo visto il modello ”puro” del polimorfismo dinamico,
quindi la classe base che ha un certo numero di funzioni virtuali pure, quindi
=0 e un distruttore virtuale e da lı̀ si derivano classi concrete che fanno l’override
delle funzioni. Possiamo anche costruire una gerarchia, nel mondo ideale anche
le classi intermedie dovrebbero essere tutte virtuali pure. In pratica spesso si
trova un mix tra interfaccia a implementazione: le classi base forniscono in parte
anche parte dell’implementazione delle classi derivate, che viene poi ereditata.
Nell’esempio delle shapes, vediamo che sia circle che rectangle hanno un dato di
tipo punto. Visto che tutte le figure avranno almeno un punto, lo posso portare
fuori e mettere in shape, perché tanto p viene ereditato. L’inizializzazione di p
avviene in shape, quindi shape deve avere un costruttore che prende un punto
p. A questo punto faccio un’altra osservazione: di default posso dire che la
mia funzione where restituisce semplicemente il punto. A questo punto la fun-
zione where non è più virtuale pura, è una funzione virtuale che mi offre anche
un’implementazione di default. Di default se chiamo su una shape la funzione
where, mi restituisce il punto. Per circle va anche bene, per rectangle faccio
l’override implementandola come voglio.
struct Shape {
Point p;
Shape(Point p) : p{p} {}
virtual ∼Shape();

53
virtual Point where() const { return p; }//default implementation
};

struct Circle : Shape {


int r;
Circle(Point p, double d) : Shape{p}, r{d} {}
∼Circle();
//where() is inherited from shape
};

struct Rectangle : Shape {


Point lr;
Rectangle(Point p1, Point p2) : Shape{p1}, lr{p2} {}
∼Rectangle();
Point where() const override { return (Shape::where() + lr) / 2; }//p
is inherited
};
In rectangle al posto di accedere direttamente a p, chiamo la funzione che mi
restituisce p. (Ci sono buone ragioni per farlo). Non è molto raccomandato, ma
si trova abbondantemente utilizzato. Il minimo è di non usare direttamente le
variabili con i nomi, ma accedere tramite funzioni.

SFML
Libreria grafica, viene utilizzata prevalentamente per fare giochi in due dimen-
sioni. Per le figure grafiche di base hanno una classe base chiamata shape
e poi hanno i vari tipi di figure geometriche: rettangolo, cerchio, figura con-
vessa generale. Shape a sua volta deriva da due classi: da drawable, una
classe con un metodo draw, e da transformable che permette di fare le ma-
nipolazioni con le posizioni. La notazione nella figura si chiama UML. La
C significa concreta, la I significa interfaccia, quindi ha almeno una funzione
virtuale pura. La freccina tratteggiata significa che c’è una derivazione solo
di interfaccia, quella continua che c’è derivazione anche di implementazione.

54
I/O Streams
C++ offre nella standard library una sottolibreria che si occupa di I/O basata
sugli streams, un’astrazione per i dispositivi di input/output. Due esempi di
questi stream sono std::cin e std::cout collegati allo standard input e output
del programma. Visto che fanno parte di una gerarchia poliformica, molta
funzionalità è implementate nelle classe base, come ad esempio gli operatori
<< e >>. Da terminale abbiamo imparato come si usa, vediamo cosa cambia
per i file. Se voglio leggerlo abbiamo un oggetto di tipo ifstream che chiamo
come voglio e metto il path di dove si trova il file. Per scriverci, uso ofstream.
Sull’ifstream uso l’operatore >>, come sull’output stream uso l’operatore >>.
Questi operatori sono ereditati da classi più primitive. Esiste anche fstream, per
leggere e scrivere. Va fatta l’operazione di chiudere il file con close(), quindi
o la faccio io o mi affido al distruttore. (Idealmente posso fare a meno di farla,
ci pensa il distruttore). Invece di essere un file o uno standard input/output
creiamo la nostra sorgente di dati che poi utilizziamo con gli operatori di dati
con i/ostringstream. Questo è molto comodo per i test o se voglio formattare
una stringa in output.

operator <<
Vediamo l’operatore di streaming in output, più semplice di quello di input. Ve-
diamo come si può implementare nella classe Complex. Questi operatori sono
implementati di solito come free functions, perché il primo parametro che viene
passato è lo stream, non l’oggetto di una classe:
class Complex {
double r;
double i;
public:
double real() const;
double imag() const;
· · ·
};
inline std::ostream& operator<<(std::ostream& os, Complex const& c)
{
os << ’(’ << c.real() << ’,’ << c.imag() << ’)’;
return os;
}

friend functions
Può capitare che le funzioni che implemento come libere, ma anche opera-
tori, abbiano bisogno di accedere alla parte privata, non è sufficiente la parte
dell’interfaccia pubblica. Il linguaggio mette a disposizione di dichiarare queste
funzioni libere come friend, amiche delle classe. Una funzione friend ha accesso

55
anche alla parte privata. Viene dichiarata dentro la definizione della classe e
può essere anche definita fuori. La cosa della friendship ha un uso più ampio,
anche una classe intera può essere amica di un’altra.

Overriding vs overloading
Per fare un override, il metodo della classe derivata deve essere identico a quello
della classe base. Se due funzioni sono solo apparentemente uguali, ma cambiano
tipo nella lista dei parametri, ho un overloading, non un overriding.
struct Base {
virtual void f(int);
};
struct Derived : Base {
void f(unsigned) override;// error: does not override };
In questo modo ho un errore di compilazione: il compilatore intuisce che volevo
fare l’override ma per qualche motivo ho sbagliato il tipo del parametro, quindi
ho fatto overloading per sbaglio e il compilatore se ne accorge. C’è una regola
che dice che le funzioni virtuali dovrebbero specificare un’opzione tra virtual,
override o final. Quello che si fa di solito è che si mette virtual nella classe
base che introduce quella funzione e in tutte le classi derivate in cui voglio
fare override scrivo esplicitamente override o final. final si può mettere come
override ma dice che le classi derivate dalla classe in cui ho dichiarato una
funzione final non possono fare l’override di quella funzione, quindi final chiude
la catena di override di quella specifica funzione. Un’intera classe può essere
dichiarata final per dare il senso di bloccare la catena di derivazione: struct
Derived final{...}.

Slicing
Guardiamo l’aspetto di copiabilità delle classi quando sono parte di una gerar-
chia. Se ho una shape che mi rappresenta un cerchio e faccio = ad un altra shape
che mi rappresenta un rettangolo, che senso vogliamo dare a questa operazione?
Vogliamo dare un senso?. Guardiamo il fenomeno dello slicing, che vale per una
gerarchia in generale, non per forza polimorfica. Se una classe base ha delle
funzioni virtuali pure è una classe astratta e non si può istanziare. Se tolgo l’=0
e quindi le funzioni virtuali hanno anche un’implementazione la mia classe base
non è più astratta, quindi potrei istanziarla. Ha senso costruire un oggetto di
tipo shape? Mi va bene che sia copiabile? Oppure dato un rettangolo rect, voglio
poter fare Shape s = rect? (sto chiamando il copy constructor con un oggetto
dinamico di tipo rettangolo). Se non faccio niente, questa cosa funziona, ma ma-
gari non voglio che funzioni, quindi posso prendere tutte le operazioni di copia
e move e mettere =delete. Supponiamo di avere un rettangolo e di avere una
funzione che prende una shape by reference(process1) e chiama shape.where(),
questo mi restituisce un punto al centro del rettangolo. Se lo prendo by value
(process2) ho fatto una copia e ho perso le informazioni del rettangolo, ho solo
una shape e quando chiamo la funzione where viene chiamata la funzione where

56
della classe Shape:
void process1(Shape& shape)
{· · · shape.where() · · ·} //mi restituisce Point{2., 4.}, ok

void process2(Shape shape)


{· · · shape.where() · · ·} //mi restituisce Point{1., 7.}, problema

auto rect = Rectangle{Point{1., 7.}, Point{3., 1.}};


process1(rect);
process2(rect);
Questo va sotto il nome di slicing: quando passo by value, affetto una parte
dell’oggetto e una parte viene buttata via. Questo deriva dal fatto che shape è
una classe base concreta, che posso istanziare.

04/05
Access control
La classe derivata cosa può vedere della classe base? Un membro di una classe
può essere:
• public: il nome del membro può essere usato ovunque senza restrizioni
• private: il nome può essere usato solo dai membri o dai friends della
classe

• protected: il nome è visibile dai membri e dei friends di una classe, come
private, ma anche dalle classi derivate di quella classe, e dai loro friends
(delle classi derivate). Interfaccia specifica verso le classi derivate.
La derivazione stessa può essere public: se sono la classe derivata ed eredito
un membro, chi usa la classe derivata come può accedere a p? Pubblico, pri-
vato o protected? Nell’inheritance is-a, quella pubblica, ciò che è pubblico
nella classe base è pubblico nella classe derivata e ciò che è protetto nella classe
base rimane protetto nella classe derivata: class Derived : public Base
{} (devo specificare public nella class, altrimenti di default è privata). Ci sono
varie forme di inheritance, come derivazione privata, dove tutto diventa privato
e anche la derivazione protected, che si vede molto raramente. La parte pro-
tected e public rappresentano comunque un’interfaccia, quindi i dati è sempre
meglio tenerli privati e fornire un’interfaccia per recuperare i dati sotto forma
di funzioni, e se queste funzioni vengono usate solo dalle classi derivate, metterle
protected.

Structural inheritance
L’inheritance può esistere anche in assenza di funzioni virtuali, la posso appli-
care anche in casi non poliformici, posso sfruttarla per riusare ed eventualmente

57
estendere l’implementazione di una classe oppure per creare un tipo distinto con
la stessa implementazione e interfaccia di un altro. Ad esempio la soluzione del
mio problema prevede di gestire un vettore di interi, ma magari nel mio dominio
vorrei qualcosa chiamata in un modo diverso, tipo Pippo. Ma non deve essere
un alias, voglio proprio un tipo distinto. Per farlo posso fare cosı̀, ereditando
tutta l’interfaccia di vector e anche tutti i costruttori:
class Pippo : public std::vector<int> {
using std::vector<int>::vector;};//eredito i costruttori

Destruction and inheritance


Nel caso di polimorfismo, il distruttore di una classe base dovrebbe essere o pub-
blico e virtuale o protected e non virtuale. Nel caso di ereditarietà strutturale,
non dinamica, dobbiamo ricordarci di non fare la delete attraverso un puntatore
alla classe base che non ha un distruttore virtuale, questo non va bene:
std::vector<int>* a= new Pippo;
delete a; //undefined behaviour.

Copying/moving and inheritance


Una delle linee guida che vengono suggerite è che le operazioni di copy e move
non dovrebbero essere pubblicamente accessibili, ma hanno bisogno di essere
accessibili alle classi derivate, in quanto quando copio una classe derivata devo
copiare anche la parte della classe base, specialmente se ci sono dei dati: se voglio
fare una copia di un rettangolo, devo copiare anche il punto che sta dentro shape
e devo usare le special member functions della classe base. Per fare ciò, dichiaro
le operazioni di copy/move protected.

58

Potrebbero piacerti anche