Programmazione Scientifica++: Lezione 4

Struttura di un Programma, Tipi, Scope e Namespace

Docente

Di cosa hai bisogno per compilare il tuo Programma C++?

  • Su macchine Linux/Unix dovresti avere il compilatore g++ installato di default
  • Su Windows puoi usare cygwin (che assomiglia a UNIX e ha gcc/g++) o WSL
  • Oppure puoi anche usare IDE moderni (Visual Studio, VS Code, CLion, ecc.)

Struttura di un Programma C++

// la tua prima applicazione C++!
#include <iostream> // richiesto per eseguire l'I/O stream in C++

// la funzione main inizia l'esecuzione del programma
int main() {
    return 0; // indica che il programma è terminato con successo
} // fine della funzione main

Direttive del Precompilatore/Preprocessore

Cos’è il preprocessore? Cosa fa?

// la tua prima applicazione C++!
#include <iostream> // richiesto per eseguire l'I/O stream in C++

// la funzione main inizia l'esecuzione del programma
int main() {
    return 0; // indica che il programma è terminato con successo
} // fine della funzione main

iostream verrà incluso prima della compilazione di questo codice!

Cosa fa il Preprocessore?

Sostituisce le direttive dell’utente con il codice sorgente richiesto (Solo pre-compilazione)

// Foo.h
class Foo {
public:
    Foo() {};
    Foo(int a) { x_ = a; };
private:
    int x_;
};
// ExamplePreprocessor.cpp
#include "Foo.h"

int main() {
    return 0;
}

Commenti in C++

I commenti sono preceduti da // Possono iniziare ovunque nel programma, sia all’inizio della riga che subito dopo un’istruzione a metà della riga

// la tua prima applicazione C++!
#include <iostream> // richiesto per eseguire l'I/O stream in C++

// la funzione main inizia l'esecuzione del programma
int main() {
    return 0; // indica che il programma è terminato con successo
} // fine della funzione main

Compilare un’applicazione C++

In questo corso useremo il compilatore gratuito g++ in tutti gli esempi

$ g++ -o Welcome Welcome.cpp
  • -o Welcome: Nome dell’eseguibile (output binario)
  • Welcome.cpp: File C++ da compilare e linkare

Alcuni aspetti base del C++

  • Tutte le istruzioni devono terminare con un punto e virgola ;
    • I ritorni a capo (carriage returns) non sono significativi e vengono ignorati dal compilatore
  • I commenti sono preceduti da //
    • I commenti possono occupare un’intera riga o trovarsi a metà della riga dopo un’istruzione
  • Qualsiasi applicazione C++ deve avere un metodo main
  • Il main deve restituire un int (intero)
    • Il valore restituito può essere utilizzato dall’utente/client/ambiente
    • Es. per capire se si è verificata una condizione di errore

Cosa succede se cambiamo il tipo del main?

// VoidMain.cpp
#include <iostream>
using namespace std;

void main() {
    // nessun tipo di ritorno
} // fine della funzione main

Il compilatore richiede che il main restituisca un valore int!

Output con iostream

iostream fornisce funzionalità di output al tuo programma

// SimpleIO.cpp
#include <iostream>
using namespace std; 

int main() { // il main inizia qui
    // stampa il messaggio sullo STDOUT (standard output)
    cout << "Primi passi in C++!" << endl;
    return 0;
} // fine del main

Input con iostream

iostream fornisce anche funzionalità di input al tuo programma

// SimpleInput.cpp
#include <iostream>
using namespace std;

int main() { // il main inizia qui
    int nIterations;

    cout << "Quante iterazioni? "; 
    cin >> nIterations;

    // stampa il messaggio sullo STDOUT
    cout << "Numero di iterazioni richieste: " << nIterations << endl;

    return 0;
} // fine del main

Tipici Errori di Compilazione Finora

// BadCode1.cpp
#include <iostream>
using namespace std; 

int main() { // il main inizia qui
    int nIterations;

    cout << "How many 
    iterations? "; // impossibile andare a capo in mezzo a una stringa!

    cin >> nIteration; // nome errato! manca la 's' finale

    // stampa il messaggio sullo STDOUT
    cout << "Numero di iterazioni richieste: " << nIterations << endl;

    return 0 // manca il ; !
} // fine del main

Dichiarazione e Definizione delle Variabili

// SimpleVars.cpp
#include <iostream>
using namespace std;

int main() { 
    int samples; // solo dichiarazione
    int events = 0; // dichiarazione e assegnazione
    
    samples = 123; // assegnazione

    cout << "Quanti samples? " ;
    cin >> samples; // assegnazione tramite I/O

    cout << "samples: " << samples
         << "\t" // inserisce un carattere di tabulazione (tab) nella stampa
         << "events: " << events
         << endl;

    return 0;
} // fine del main

Richiamo: la Divisione Intera

Warning

Lo sapete già dal C. Lo ripetiamo perché, nel calcolo scientifico, è il bug che costa più ore di debug di qualunque altro — e non dà nessun errore.

int eventi_totali = 7;
int eventi_buoni  = 5;

double efficienza = eventi_buoni / eventi_totali;   // vale 0.0, non 0.714

Se entrambi gli operandi sono interi, / è la divisione intera: 5 / 7 fa 0, con resto scartato. Il risultato viene poi convertito in double, ma il danno è già fatto. Il programma compila, gira, e stampa un’efficienza pari a zero.

Altri casi che vi capiteranno:

double media = somma / N;          // se somma e N sono int: troncata
double passo = 1 / 100;            // vale 0.0: entrambe le costanti sono int
double frazione = i / (N - 1);     // dentro un ciclo: sempre 0 tranne all'ultimo giro

La Cura: Promozione Esplicita

Basta che uno dei due operandi sia double: C++ promuove automaticamente anche l’altro, e la divisione diventa reale.

double efficienza = static_cast<double>(eventi_buoni) / eventi_totali;  // 0.714
double passo      = 1.0 / 100;                                           // 0.01
double frazione   = static_cast<double>(i) / (N - 1);

Tre regole pratiche:

  1. Nelle costanti scrivete sempre il punto: 1.0, non 1. Costa nulla e toglie ogni ambiguità.
  2. Con le variabili usate static_cast<double>(x): è esplicito, si cerca con grep, e non modifica x — produce un temporaneo.
  3. Il cast C-style (double) x funziona ma è da evitare: non si distingue dalle conversioni pericolose fra puntatori, che vedremo nella Lezione 16.

Tip

Nel Laboratorio 02, questa stessa settimana, vi chiederemo di far vedere l’errore a schermo con 5 / 2. Sembra banale: è il modo migliore per non dimenticarsene mai più.

Cicli e iterazioni in C++

int main() { // il main inizia qui
    int nIterations;
    cout << "Quante iterazioni? ";
    cin >> nIterations;

    int step;
    cout << "passo (step) dell'iterazione? " ;
    cin >> step;

    for(int index=0; index < nIterations; index+=step) {
        cout << "indice: " << index << endl;
    }
    
    return 0;
} // fine del main

Problemi con cin

// tinput_bad.cc
#include <iostream>
using namespace std;

int main() {
    cout << "iterazioni? ";
    int iters;
    cin >> iters;
    
    cout << "richieste " << iters << " iterazioni\n";
    return 0;
}

Terminale:

$ g++ -Wall -o tinput_bad tinput_bad.cc
$ ./tinput_bad
iterazioni? 23
richieste 23 iterazioni

$ ./tinput_bad
iterazioni? dfed
richieste 0 iterazioni

Warning

Cosa succede se inseriamo una stringa invece di un numero? Dal C++11 >> azzera la variabile e alza un flag di errore. Il programma non si accorge di niente e prosegue con iters = 0: nessun avviso, nessun crash, un numero plausibile. È peggio di un valore assurdo, che almeno si nota.

1° Errore: Variabili non inizializzate

int main() {
1    int iters;
    cout << "iters prima: " << iters << endl;
    
    cout << "iterazioni? ";
    cin >> iters;
    
    cout << "richieste " << iters << endl;
    return 0;
}
1
Valore random (spazzatura) poiché la memoria non è stata inizializzata: questa riga è undefined behaviour, può stampare qualsiasi cosa. La lettura fallita che segue invece azzera iters, e la spazzatura sparisce.

Terminale:

$ ./tinput_bad2
iters prima: 134514841
iterazioni? er
richieste 0

Verificare lo stato di cin

int main() {
    int iters = 0;
1    cin >> iters;
    
2    if(cin.fail()) {
        cout << "cin fallito!" << endl;
    }
    
    cout << "richieste " << iters << endl;
    return 0;
}
1
Se diamo in pasto del testo a una variabile int, l’operazione fallisce silenziosamente.
2
Usiamo .fail() per intercettare l’errore e gestirlo!

Terminale:

$ ./tinput
iterazioni? sfee
cin fallito!
richieste 0

Dichiarazioni di Controllo

#include <iostream>
using namespace std;

int main() { 
    if( 1 == 0 ) cout << "1==0" << endl;
    
    if( 7.2 >= 6.9 ) cout << "7.2 >= 6.9" << endl;
    
    bool truth = (1 != 0);
    if(truth) cout << "1 != 0" << endl;
    
    if( ! ( 1.1 >= 1.2 ) ) cout << "1.1 < 1.2" << endl;
    
    return 0;
} 

Scope delle Variabili

  • Lo scope (visibilità) di un nome è il blocco in cui quel nome è valido.
  • Un blocco è delimitato da parentesi graffe { }.
double compute_loss() {
1    double error = 2.5;
    return error;
}

int main() {
2    double prediction = 3.0;
    double current_loss = compute_loss();
    
    std::cout << prediction << " " << current_loss << "\n";
3    // std::cout << error;
    return 0;
}
1
error esiste solo all’interno di compute_loss().
2
prediction esiste in tutto il main().
3
ERRORE DI COMPILAZIONE: error non è in questo scope!

Variable Shadowing (Ombreggiatura)

Cosa succede se dichiariamo una variabile con lo stesso nome in uno scope nidificato? La variabile più interna “nasconde” quella esterna!

#include <iostream>

int main() {
    double x = 10.0; // Scope esterno
    
    { // Inizio scope interno
1        double x = 5.0;
        std::cout << "x interno: " << x << "\n"; 
2    }
    
    std::cout << "x esterno: " << x << "\n"; 
    return 0;
}
1
Questa x “nasconde” temporaneamente la x esterna!
2
Fine scope interno, la x interna viene distrutta per sempre.

Terminale:

x interno: 5.0
x esterno: 10.0

Cos’è un Namespace?

  • Previene le “collisioni di nomi”: due librerie diverse possono avere una funzione chiamata print().
namespace neural {
    class layer;
    class network;
    void activate();
}

namespace math {
    class matrix;
    void activate();
}

Usare i namespace: l’operatore ::

#include <cmath>

namespace neural {
    // la sigmoide: quella che useremo davvero nella rete
    double activate(double x) { return 1.0 / (1.0 + std::exp(-x)); }
}

namespace math {
    // la tangente iperbolica: un'altra scelta possibile
    double activate(double x) { return std::tanh(x); }
}

int main() {
    // Usa "::" (Operatore di Risoluzione di Scope)
1    double out1 = neural::activate(-2.5);
    
2    double out2 = math::activate(-2.5);
    return 0;
}
1
Richiama esplicitamente la activate del namespace neural: vale 0.076.
2
Richiama l’omonima di math: vale −0.987. Stesso nome, funzioni diverse, nessuna collisione.

Note

L’operatore :: si chiama Scope Resolution Operator. Lo vediamo sempre in std::cout, perché cout appartiene al namespace std.

Direttiva using namespace

#include <iostream>
#include <cmath>

namespace neural {
    double activate(double x) { return 1.0 / (1.0 + std::exp(-x)); }
}

1using namespace std;

int main() {
    double out1 = neural::activate(3.5);
    
2    cout << "neural::activate = " << out1 << endl;
    return 0;
}
1
Rende tutti i nomi del namespace std disponibili senza dover scrivere std::.
2
Il compilatore ora sa che cout ed endl fanno parte di std.

Attenzione a using multipli!

Creare ambiguità è facilissimo:

using namespace math;
using namespace neural;
using namespace std;

int main() {
    double input = 3.5;
    
    // Ambiguo! Di quale metodo activate() stiamo parlando?
1    double out1 = activate(input);
    
    return 0;
}
1
Errore del compilatore: call of overloaded 'activate(double&)' is ambiguous — con la reference, perché input è una variabile e non una costante.

Important

Regola d’oro: Mai inserire using namespace std; in un file header (.h o .hpp). Potreste inquinare i file di chi usa la vostra libreria!

Esercizi lampo: cosa stampa?

Tre frammenti, un minuto ciascuno. Rispondete prima di girare la slide: in tutti e tre la risposta istintiva è sbagliata.

1. L’utente digita ciao, non un numero.

int n = 42;
std::cin >> n;
std::cout << n << "\n";

2.

int a = 5, b = 2;
std::cout << static_cast<double>(a / b) << "  "
          << static_cast<double>(a) / b << "\n";

3.

int i = 0;
for (int i = 0; i < 3; ++i) { /* ... */ }
std::cout << i << "\n";

Esercizi lampo: le risposte

1. Stampa 0, non 42. Dal C++11 una lettura fallita azzera la variabile: il valore che c’era prima è perso. Se vi serve, controllate cin.fail() prima di usare n.

2. Stampa 2 2.5. Nel primo caso a / b è già stato calcolato fra interi: il cast arriva troppo tardi e promuove un 2 in cui non c’è più niente da recuperare. Il cast va messo su un operando, mai sul risultato.

3. Stampa 0, non 3. L’i dichiarato nella testa del for vive solo dentro il ciclo e ombreggia quello esterno, che nessuno ha mai toccato. È per questo che esiste l’opzione -Wshadow: -Wall da sola non dice niente.