Maurizio Tomasi
Martedì 29 Ottobre 2024
La spiegazione dettagliata degli esercizi si trova qui: carminati-esercizi-06.html.
Come al solito, queste slides, che forniscono suggerimenti addizionali rispetto alla lezione di teoria, sono disponibili all’indirizzo ziotom78.github.io/tnds-tomasi-notebooks.
FunzioneBase
L’unico modo in cui il C++ implementa il polimorfismo durante
l’esecuzione è attraverso i metodi virtual
.
(I template permettono polimorfismo in fase di compilazione, e sono quindi usati in ambiti diversi).
Una classe derivata può ridefinire uno dei metodi della classe
genitore se questo è indicato come virtual
, usando la
parola chiave override
.
#include <iostream>
struct Animal {
// Dichiaro `greet` come un metodo che può essere sovrascritto
virtual void greet() const { std::cout << "?\n"; }
};
struct Dog : public Animal {
// Chiedo al C++ di sovrascrivere `greet` tramite `override`
void greet() const override { std::cout << "Woof!\n"; }
};
struct Cat : public Animal {
// Chiedo al C++ di sovrascrivere `greet` tramite `override`
void greet() const override { std::cout << "Meow!\n"; }
};
#include <string>
int main(int argc, char *argv[]) {
Animal *animal{}; // Qui abbiamo proprio bisogno di *
// Legge il nome dell'animale dalla linea di comando
std::string name{argc > 1 ? argv[1] : ""};
if (name == "cat") {
animal = new Cat();
} else if (name == "dog") {
animal = new Dog();
}
if (animal)
animal->greet();
}
Nel main
, la chiamata ad
animal->greet()
non permette di capire quale metodo
greet
verrà usato guardando solo il codice: la scelta viene
fatta a runtime, a seconda del valore di argv
:
$ ./test cat
Meow!
$ ./test dog
Woof!
$ ./test bird
$
Questo meccanismo si chiama dynamic dispatch
(indirizzamento dinamico). Nel caso del C++ è un dynamic
single dispatch, perché la chiamata viene
decisa solo in funzione del tipo dell’oggetto puntato da
animal
. (Altri linguaggi come Julia usano tutti i tipi dei
parametri della funzione, e sono quindi più versatili).
virtual
e override
La parola virtual
è obbligatoria solo all’interno della
classe base:
struct Animal {
virtual void greet() const { std::cout << "?\n"; }
};
struct Dog : public Animal {
// I can repeat "virtual" here, but it's optional
virtual void greet() const override { std::cout << "Woof!\n"; }
};
struct Cat : public Animal {
// You usually avoid repeating "virtual", if there is "override"
void greet() const override { std::cout << "Meow!\n"; }
};
virtual
e override
Per compatibilità con le vecchie versioni del C++, anche
override
può essere tralasciato:
Questo è comune in codici C++ particolarmente vecchi, ma oggigiorno è considerata una pessima pratica.
Nell’Esercizio 6.1 di oggi si usano i puntatori nel seguente codice:
Si usano puntatori (o reference) quando avviene questo:
Particella
)…Elettrone
), che non è noto in fase di
compilazione.Usando un puntatore e new
, si possono specificare i
due tipi Particella
ed Elettrone
separatamente:
Senza puntatore, non ci sarebbe questa possibilità:
Se non ci si trova nella situazione descritta, è meglio evitare di
usare i puntatori: il codice è più semplice da leggere, e non c’è
possibilità di avere segmentation fault
causati
dall’accesso a puntatori nulli.
L’esercizio richiede di trovare gli zeri della funzione f(x) = 3x^2 + 5x - 2, che è un problema risolubile analiticamente: x_{1/2} = \frac{-5 \pm \sqrt{25 + 24}}6 = \begin{cases} -2,\\ \frac13. \end{cases}
// You might even pass a reference to `Solutore & s` and call in `main`
// test_zeroes(Bisezione{});
// test_zeroes(Secante{});
// test_zeroes(Newton{});
void test_zeroes() {
Bisezione s{};
Parabola f{3, 5, -2}; // Zeroes for this function are known: x₁ = −2, x₂ = 1/3
s.SetPrecisione(1e-8);
assert(are_close(s.CercaZeri(-3.0, -1.0, f), -2.0)); // Zero is within (a, b)
assert(are_close(s.CercaZeri(-2.0, 0.0, f), -2.0)); // Zero is at a
assert(are_close(s.CercaZeri(-4.0, -2.0, f), -2.0)); // Zero is at b
assert(are_close(s.CercaZeri(0.0, 1.0, f), 1.0 / 3)); // Do NOT write 1 / 3 !
cerr << "Root finding works correctly! 🥳\n";
}
Nell’algoritmo di bisezione occorre verificare quando f(a) e f(b) sono di segno concorde.
La scrittura
non è consigliabile, perché se f(a)
o f(b)
sono molto piccoli, il risultato potrebbe essere nullo. Meglio usare una funzione
sign
Il metodo di bisezione fallisce se le ipotesi del teorema degli zeri non valgono. Cosa fare in questo caso?
Ricordiamo che esistono due tipi di errori:
Errori dell’utente: è l’utente che ha sbagliato a usare il programma, e deve rimediare facendolo ripartire con gli input giusti;
Errori del programmatore: è il programmatore che deve rimediare modificando il programma e ricompilandolo.
Oggi implementiamo due esercizi in cui il metodo di bisezione può fallire per colpa dell’utente (esercizio 6.2) o del programmatore (esercizio 6.3). Come facciamo?
$ ./esercizio-6.2 4 6
Errore, il teorema degli zeri non è valido nell'intervallo [4, 6]. Usa un altro intervallo
$ ./esercizio-6.3
fish: Job 1, './esercizio-6.3' terminated by signal SIGABRT (Abort)
(Se un programma termina invocando abort()
, eseguendolo
all’interno di QtCreator in modalità
debugging si può ispezionare il valore delle variabili al momento in cui
è andato in crash: molto utile per correggere il bug!)
Scrivere un messaggio di errore e invocare abort()
:
bene se l’errore è del programmatore, male se l’errore è dell’utente!
😠
Restituire un valore fissato (es., zero): molto ambiguo, come fa l’utente a sapere se la funzione si annulla veramente per x = 0 o se c’è stato un errore? 😠
Accettare un parametro aggiuntivo bool &found
(reference!) per CercaZeri
:
(In alternativa si può dichiarare found
variabile membro
di Solutore
).
Con il terzo approccio è possibile differenziare la “reazione”
all’errore nel main
dei due esercizi
// main() dell'esercizio 6.2 (errore dell'utente):
double xmin{stod(argv[1])}, xmax{stod(argv[2])};
bool found;
double x{bisezione.CercaZeri(xmin, xmax, f, found)};
if (! found) {
fmt::println("Errore, il teorema degli zeri non è valido nell'intervallo "
"[{}, {}]. Usa un altro intervallo", xmin, xmax);
return 1;
}
// main() dell'esercizio 6.3 (errore dell'utente):
double xmin{ … }; // Formula matematica implementata dal programmatore;
double xmax{ … }; // Idem
bool found;
double x{bisezione.CercaZeri(xmin, xmax, f, found)};
if (! found) {
abort();
}
std::expected
(1/5)Con il C++23 è stato introdotto il tipo std::expected
,
che è stato mutuato da linguaggi come Rust e OCaml. (Nella compilazione
bisogna quindi usare -std=c++23
, non
-std=c++20
!)
Il template std::expected<T, U>
rappresenta il
concetto “di solito questa variabile è del tipo T
, ma se
c’è un errore allora è del tipo U
”:
std::expected
(2/5)Una variabile std::expected
può essere inizializzata
usando un valore del primo tipo (T
) senza
bisogno di sintassi particolari:
Per inizializzarla al valore “erroneo”, bisogna usare la funzione
std::unexpected
:
std::expected
(3/5)Nel main()
è laborioso definire il tipo della
variabile che riceve il risultato:
Il C++ fornisce (a partire dal C++11) la keyword
auto
, che dice al compilatore: “sai benissimo tu qual è il
tipo di ritorno, quindi non farmelo specificare!”. Quindi possiamo
scrivere:
std::expected
(4/5)Si controlla se la variabile è “giusta” o “sbagliata” col metodo
has_value()
:
C’è anche la comoda scorciatoia di usare result
come
una variabile booleana:
std::expected
(5/5)Il metodo value()
estrae il valore giusto, il metodo
error()
il valore erroneo:
Invece di value()
si può usare l’operatore di
deferenziazione *result
:
std::expected
nella bisezionestd::expected<double, string> // Ideally we return a `double`, unless there are problems
Bisezione::CercaZeri(double xmin,
double xmax,
const FunzioneBase * f) {
double signfa{sign(f->Eval(xmin))}, signfb{sign(f->Eval(xmax))};
if(signfa == 0) return xmin;
if(signfb == 0) return xmax;
if(signfa * signfb > 0) {
return std::unexpected(fmt::format("Invalid range [{}, {}]", xmin, xmax));
}
// ...
return x; // `x` is a boring `double`
}
std::expected
nel main
Nel main
dell’esercizio 6.2
stampiamo l’errore in maniera user-frendly:
Nell’esercizio
6.3 invece chiamiamo abort()
(in
<cstdlib>
):
Nell’esercizio 6.4 si illustra un modo alternativo di implementare la bisezione, che non usa il polimorfismo.
Questo approccio è importante da imparare: