Laboratorio di trattamento numerico dei dati sperimentali - Maurizio Tomasi turno A2 Giovedì 9 Novembre 2017 - cosmo
←
→
Trascrizione del contenuto della pagina
Se il tuo browser non visualizza correttamente la pagina, ti preghiamo di leggere il contenuto della pagina quaggiù
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