Laboratorio di trattamento numerico dei dati sperimentali - Maurizio Tomasi turno A2 Giovedì 9 Novembre 2017 - cosmo

Pagina creata da Dario Iorio
 
CONTINUA A LEGGERE
Laboratorio di trattamento numerico dei
dati sperimentali
Maurizio Tomasi ﴾turno A2﴿

Giovedì 9 Novembre 2017
Particolarità nella sintassi del C++
Chiamate di costruttori senza parametri
Il C++ è un linguaggio con una grammatica estremamente
complessa, e in più punti presenta ambiguità.

Un caso tipico è quello dell'invocazione di costruttori senza
parametri.
Chiamate di costruttori senza parametri
Consideriamo questo esempio:

 class A {
 public:
    A() {}
 };

 int main()
 {
    A a();
 }

L'istruzione A a() è ambigua, e non produce l'effetto desiderato
﴾creazione di una variabile a di tipo A ﴿.
Chiamate di costruttori senza parametri
Perché succede questo? Perché A a() può essere interpretato in
due modi:

  1. Creazione di una variabile di tipo A invocando il costruttore
     senza parametri;
  2. Dichiarazione di una funzione di nome a , che ritorna un
     valore A .

Il secondo caso è quello che avevamo visto nelle prime lezioni:

 double somma(double a, double b);

 // ...

 double somma(double a, double b) {
     return a + b;
 }
Chiamate di costruttori senza parametri
Lo standard C++ prescrive che, in caso di ambiguità, il compilatore
debba sempre preferire la seconda interpretazione: quindi il codice

 int main()
 {
    A a();
 }

non crea alcuna variabile a ! La soluzione consiste nell'invocare il
costruttore senza () :

 int main()
 {
    A a; // This works as expected
 }
Chiamate di costruttori senza parametri
Ci sono altri casi in cui l'ambiguità non c'è, e in cui quindi le
parentesi sono opzionali:

 A * a = new A();
 A * b = new A;

Ed infine, quando si definisce il costruttore, le parentesi sono
obbligatorie:

 class A {
 public:
     A();
 };
Curiosità
Un altro esempio famoso di ambiguità nel C++ è la notazione dei
template ﴾non affrontati in questo corso﴿, che usa le parentesi
angolari  . La seguente definizione è ambigua:

 vector a;

perché le doppie parentesi angolari >> indicano l'operatore di
bitshift a destra. ﴾Ma il C++11 permette questa scrittura﴿.

Altro esempio: la scrittura

 foo a>

non viene interpretata come foo o foo ﴾a seconda
che 3 > a sia vero o falso﴿, ma come la definizione di una variabile
 a di tipo foo , seguita da un carattere > che causa un errore di
sintassi.
Curiosità
Le ambiguità del linguaggio hanno spesso un impatto significativo
anche sui tempi di compilazione, perché il compilatore si trova
spesso a dover compiere decisioni complesse.

C'è stata una famosa corrente di pensiero, il cui capostipite è
Niklaus Wirth, che ha invece proposto l'uso di linguaggi di
programmazione dalla sintassi estremamente semplice e priva di
ambiguità, per velocizzare le compilazioni e ridurre il carico mentale
del programmatore.

È su queste basi che lo stesso Wirth ha ideato in sequenza i
linguaggi Pascal, Modula, Modula‐2 ed Oberon, semplificando
progressivamente la sintassi ﴾invece di complicarla!﴿. Il caso estremo
è Oberon, un linguaggio estremamente elementare che Wirth ha
comunque usato per scrivere un intero sistema operativo!
Metodi virtuali
Uso di virtual
La parola chiave virtual ha una serie di peculiarità. Consideriamo
questo esempio:

 class A {
 public:
     A() {}
     virtual void print() { std::cout
Uso di virtual
Dal punto di vista del compilatore non c'è quindi alcuna differenza
nello specificare o meno virtual nelle classi derivate: è sufficiente
che sia stato usato nel metodo della classe base.

Ma è comunque buona norma usarlo anche nelle classi derivate,
perché così il codice è più facile da leggere per il programmatore.
C++11 e funzioni virtuali.
I nuovi standard del linguaggio C++, a partire da quello rilasciato
nel 2011 ﴾chiamato «C++11»﴿ hanno introdotto alcuni strumenti
per rendere più semplice l'uso dei metodi virtuali.

Purtroppo, i compilatori disponibili in laboratorio non supportano
ancora questo standard. Ma è comunque utile illustrare queste
modifiche, perché indirettamente spiegano quali sono errori molto
comuni nell'uso di virtual .
C++11 e funzioni virtuali
Un problema molto comune è quello di credere di avere
reimplementato un metodo in una classe derivata, ma di non averlo
in realtà fatto:

 class A {
 public:
     virtual double eval(double x) const = 0;
 }

 class B : public A {
 public:
     virtual double eval(double x);
 };

Il metodo eval di B non è una ridefinizione di quello di A ,
perché manca il const : quindi B è ancora una classe astratta!
C++11 e funzioni virtuali
Il nuovo standard C++11 offre una nuova keyword, override , che
mette al riparo da questo problema:

 class A {
 public:
     virtual double eval(double x) const = 0;
 }

 class B : public A {
 public:
     virtual double eval(double x) override;
 };

Se si compila il programma, il compilatore dà errore perché
 B::eval non ridefinisce alcunché.
C++11 e funzioni virtuali
Il C++11 definisce anche un'altra keyword: final .

 class A {
 public:
     virtual double fn(double x);
 }

 class B : public A {
 public:
     virtual double fn(double x) final;
 };

A causa di final , se una nuova classe C derivata da B provasse a
reimplementare di nuovo fn darebbe errore.
Ricerca di zeri
Numeri floating‐point
Differenti architetture hardware rappresentano i numeri floating‐
point in modi diversi.

Una rappresentazione molto usata è quella specificata dallo
standard IEEE 754, che ha una particolarità nella gestione del segno.

Dei 32/64 bit usati per un numero floating point, il primo bit s
codifica il segno del numero, che è (−1)s . Quindi s   = 0 indica un
numero positivo, s   = 1 un numero negativo.
Rappresentazione dello zero
Il bit s è sempre presente, anche se il numero da rappresentare è
zero ﴾che in matematica non ha segno﴿.

Questo implica che la rappresentazione IEEE 754 ammette sia lo
zero positivo che lo zero negativo!
Rappresentazione dello zero
Considerate questo programma:

 #include 

 void underflow(double start) {
     double x = start;
     while (x != 0.0) {
         x /= 2;
     }
     std::cout
Rappresentazione dello zero
L'esecuzione del programma produce questo output:

 0
 ‐0

Nel primo caso lo zero è il prodotto di un underflow ottenuto
dividendo un numero positivo tante volte.

Nel secondo caso il numero è negativo, e il segno viene preservato
anche se il numero è così piccolo che per il computer è diventato
zero.
Uguaglianze tra zeri
Nei confronti in C++ però tutti gli zeri sono uguali!

 #include 

 int main() {
     double x = 1.0;
     double y = ‐1.0;

     while (x != 0.0) {
          x /= 10.0;
          y /= 10.0;
     }

     std::cout
Uguaglianze tra zeri
L'output del programma è

 x   = 0, y = ‐0
 x   == y? 1
 x   > 0? 0
 y   < 0? 0

﴾ricordate che il valore 1 è equivalente a true , mentre 0 è
equivalente a false ﴿.

Anche se la rappresentazione dei due numeri è diversa ﴾ 0 in un
caso, ‐0 nell'altro﴿, quando si confrontano x e y il compilatore
riporta che sono identici. Inoltre, non è possibile verificare il segno
con i confronti < 0 e > 0 .
Il teorema degli zeri
Nello svolgimento degli esercizi previsti per oggi dovete verificare
che valga l'ipotesi del teorema degli zeri, ossia che dato un
intervallo [a, b] e una funzione f definita e continua sull'intervallo
[a, b], la funzione sia tale che

                           f (a) ⋅ f (b) < 0.

Gli algoritmi di bisezione e della secante procedono iterativamente
a restringere l'intervallo [a, b], «costringendo» via via la posizione
del punto x0   ∈ [a, b] in cui f (x0 ) = 0.
Il teorema degli zeri
C'è però un possibile problema di arrotondamento, perché
nell'intorno dello zero x0 la funzione f (x) tende a zero, così che il
prodotto

                             f (a) ⋅ f (b)

è sempre più piccolo. Ad un certo punto potrebbe avvenire che
f (a) = 0 oppure f (b) = 0: anche se il risultato è uno zero con il
segno corretto, non possiamo verificarlo perché in C++ i confronti
tra gli zeri sono particolari ﴾come abbiamo visto﴿.
Il teorema degli zeri e il C++
Quanto detto ha un'implicazione nell'implementazione dei metodi
iterativi di ricerca di zeri:

 while(b ‐ a > desired_precision) {
     if(f(a) * f(b) >= 0.0) {
         std::cerr
Il teorema degli zeri e il C++
Abbiamo visto che gli zeri con segno non sono gestibili come valori
ordinari in C++, perché i confronti ﴾ == , < , > ﴿ dànno risultati non
utili.

Per metterci al riparo da eventuali problemi, possiamo
implementare la funzione sign ﴾non presente nel C++﴿:

 double sign(double x) {
     if (x == 0) {
         return 0;
     } else {
         return x > 0 ? 1 : ‐1;
     }
 }
Funzioni aggiuntive in C++11
A partire dallo standard C++11, la libreria cmath include alcune
nuove funzioni per gestire i numeri floating‐point che fanno al caso
nostro. Una di questi è la funzione std::copysign(a, b) , che
restituisce il numero a con il segno di b . Essa presenta alcuni
vantaggi:

  1. È più veloce della nostra funzione sign perché fa una
     semplice copia del bit del segno, senza usare alcun if .
  2. Funziona anche se b è uno zero con segno.
Funzioni aggiuntive in C++
Il confronto f(a) * f(b) < 0 del nostro codice può quindi
diventare questo:

 std::copysign(1.0, f(a)) * std::copysign(1.0, f(b)) < 0

Ciascuna delle due chiamate a std::copysign restituisce 1.0
oppure ‐1.0 , a seconda del segno di f(a) / f(b) : il prodotto tra i
due numeri è quindi sempre 1.0 oppure ‐1.0 , senza rischi di
underflow indesiderati.
Compilatori C++11
Uso del compilatore C++11
Come detto, il compilatore C++ disponibile sui computer del
laboratorio non supporta le funzionalità del C++11. Se siete
interessati ad usare programmi C++11, avete alcune possibilità:

  1. Eseguendo da linea di comando il comando scl enable
     devtoolset‐1.1 bash viene abilitata una versione più recente di
      gcc e g++ che supporta il C++11 ﴾fate logout per tornare
     alla versione precedente﴿;
  2. Si usa il sito TutorialsPoint, che offre la possibilità di digitare ed
     eseguire programmi C++ usando la versione più recente del
     compilatore GCC ﴾al momento la 7.2, rilasciata nell Agosto
     2017﴿.
  3. Installate sul vostro computer personale una versione
     aggiornata di un compilatore C++.
Uso del compilatore C++11 in laboratorio
Se abilitate la versione più recente del compilatore da un terminale
del laboratorio, ci sono alcune cose di cui dovete tenere conto:

  1. La versione del GCC che viene abilitata è la 4.7.2 ﴾del 2012﴿, che
     non attiva ancora il supporto al C++11 di default. Dovete
     quindi sempre ricordarvi di passare a g++ il flag ‐‐std=c++11 .
  2. Se usate un Makefile con regole implicite, dovete aggiungere
     in cima la riga CXXFLAGS = ‐‐std=c++11 ﴾v. la prossima slide﴿.

Una curiosità: solo con il GCC 6.1, rilasciato nell'Aprile 2016, la
versione usata come default del C++ è stata aggiornata. Se fino ad
allora l'assenza del flag ‐‐std implicava l'uso dello standard
C++98 ﴾sì, del 1998!﴿, a partire dalla versione 6.1 l'assenza del flag
assume l'uso dello standard C++14.
C++11 nei Makefile
Questo è un esempio che abilita la compilazione di myprog.cc
usando il nuovo standard C++11:

 CXXFLAGS = ‐‐std=c++11
 LDFLAGS = ‐lstdc++

 myprog: myprog.cc

La variabile CXXFLAGS indica quali flags vanno passati al GCC
quando si compilano file C++ ﴾forse vi aspettavate che la variabile
si chiamasse CPPFLAGS , ma questo nome è già usato in un altro
contesto﴿.
Puoi anche leggere