Istruzioni condizionali

Fare qualcosa d’intenzionale implica capire che esistono delle alternative, e sceglierne una; e questi sono attributi dell’intelligenza artificiale

Le istruzioni condizionali sono l’elemento più importante del codice.

Ogni programma deve saper reagire correttamente al variare delle condizioni di utilizzo; per far ciò, si utilizzano le cosiddette istruzioni condizionali, che permettono di definire il comportamento del sistema a seconda che una determinata condizione si riveli vera o falsa.
Il C++ possiede due tipi di istruzione condizionale: le sequenze if-else e l’istruzione switch.
La forma generale delle istruzioni if-else è la seguente:

if ( <condizione> ) {
    // istruzioni da eseguire se la condizione è vera
} else {
    // istruzioni da eseguire se la condizione è falsa
}

Se l’espressione condizionale all’inizio del codice è vera, il programma eseguirà il primo blocco di istruzioni; se no, eseguirà il secondo blocco di istruzioni.

if ( a > 8 ) {
    cout << "maggiore" << endl;
} else {
    cout << "minore" << endl;
}

Se la condizione falsa non richiede alcuna azione specifica, il secondo blocco di istruzioni può essere omesso:

typedef Importo unsigned long;

Importo raddoppiaStipendio(Importo stipendioCorrente)
{
    if ( stipendioCorrente > 0 ) {
        stipendioCorrente *= 2; 
    }    
    return stipendioCorrente;   
}

Allo stesso modo, le parentesi graffe possono essere omesse se il blocco istruzioni che racchiudono è costituito da una singola istruzione:

if ( a > 8 ) 
    cout << "maggiore" << endl;
else
    cout << "minore" << endl;

Personalmente, trovo che questa forma sia inelegante e che renda il codice meno chiaro, favorendo quindi gli errori. La utilizzo solo nelle istruzioni di gestione degli errori, dove il flusso del programma si interrompe bruscamente, perché l’aspetto sgraziato dell’istruzione evidenzia l’eccezione, rendendo il codice più auto-esplicativo.

if ( divisore == 0 ) 
    throw std::runtime_error("errore");

Se le condizioni da valutare sono più di due, si possono concatenare più istruzioni condizionali utilizzando l’istruzione else if, che permette di definire una condizione alternativa alla prima e di associarle un blocco di codice. Anche in questo caso, si può chiudere la sequenza con un’istruzione else, definendo un blocco di istruzioni da eseguire se non si verifica nessuna delle condizioni previste.

if ( <prima condizione> ) {
    /*
     * istruzioni da eseguire se 
     * la prima condizione è vera
    */
} else if ( <seconda condizione> ) {
    /*
     * istruzioni da eseguire se 
     * la prima condizione è vera
     */
} else {
    /*
     * istruzioni da eseguire se nessuna
     * delle due condizioni è vera
     */
}

Le istruzioni if-else influenzano la leggibilità del codice; è una cosa di cui il buon programmatore deve sempre tenere conto. Il C++ è un linguaggio indipendente dalla formattazione, quindi, una stessa istruzione può essere scritta in molte maniere diverse:

if ( <condizione> )
{
    ...
}
else
{
    ...
}

o, pure:

if ( <condizione> ) {
    ...
} else {
    ...
}

o perfino:

if ( <condizione> ) { ...} else { ... }

Se le istruzioni sono poche e semplici, una forma vale l’altra (fatte salve le questioni di stile, ovviamente), ma se il flusso del programma fosse, come di solito avviene, più complesso, è necessario fare in modo che la forma dell’istruzione semplifichi la scrittura, la lettura e un’eventuale correzione del codice.
Immagina un brano di codice che debba fare una verifica all’inizio dell’elaborazione e, a seconda dell’esito, eseguire una sequenza di istruzioni o inviare un messaggio di errore:

if ( <condizione> ){
    /*
     * righe di codice da
     * eseguire in caso la
     * condizione sia vera
     */
} else {
    /* gestione dell'errore */
}

Se le istruzioni da eseguire in caso di buon successo della verifica sono poche e semplici, questa sequenza non darà problemi, ma se, al contrario, le istruzioni fossero tante e complesse, leggendo il codice potresti arrivare all’istruzione else e non ricordarti più a quale condizione fosse associata. In questi casi, io preferisco la forma:

if ( <errore> ) {
    /* gestione dell'errore */
} else {
    /*
     * righe di codice da
     * eseguire in caso la
     * condizione sia vera
     */
}

Dato che la gestione dell’errore non richiederà mai più di qualche riga di codice, potrai capire a colpo d’occhio tutto il flusso del programma, indipendentemente dalla lunghezza del secondo blocco di istruzioni.
Tutto questo, ovviamente, non vuole essere né un invito né una giustificazione per la scrittura di istruzioni complesse. A meno che non sia necessario limitare le chiamate a funzione per garantire un’alta velocità di esecuzione, è sempre meglio scomporre il flusso del programma in una serie di funzioni distinte e specializzate. Renderai il tuo programma un po’ più lento (o, meglio: un po’ meno veloce), ma il codice sarà molto più facile da leggere o da modificare.
Immagina adesso un brano di codice che richieda molte condizioni if concatenate:

esito = 0;

if ( <condizione 1> ) {
    esito = 1;
} else if ( <condizione 2> ) {
    esito = 2;
} else if ( <condizione 3> ) {
    esito = 3;
} else {
    esito = 9;
}

return esito

Questa forma, per quanto corretta e formalmente ineccepibile, potrebbe rivelarsi difficile da gestire se le condizioni da considerare fossero molto complesse o numerose. Il buon programmatore, allora, può decidere di contravvenire alla (giusta) norma che prescrive di non inserire delle istruzioni return all’interno del codice, e scrivere la sequenza in questo modo:

esito = 0;

if ( <condizione 1> ) {
    return 1;
}
if ( <condizione 2> ) {
    return 2;
} 
if ( <condizione 3> ) {
    return 3;
} 

return 9

Non ti sto dicendo che sia giusto scrivere così e vedi da solo che il codice è rozzo e inelegante, ma ci potrebbero essere dei casi in cui sia questa, la forma da preferire. Per esempio, per un sistema che generi del codice in maniera automatica, è molto più semplice gestire delle istruzioni if isolate che delle condizioni if-else concatenate. Pensa a una stored-procedure che debba controllare l’integrità referenziale dei parametri ricevuti:

CREATE FUNCTION utente_insert (
  _id_classe INTEGER
, _id_gruppo INTEGER
, _username  VARCHAR(255)
, _cognome   VARCHAR(80)
, _nome      VARCHAR(80)
)
RETURNS INTEGER DETERMINISTIC
BEGIN
    DECLARE _id    INTEGER DEFAULT -1;
    DECLARE _count INTEGER DEFAULT 0;

    SELECT count(*) INTO _count FROM classe WHERE (id = _id_classe);
    IF _count = 0 THEN
        RETURN -2;
    END IF;

    SELECT count(*) INTO _count FROM gruppo WHERE (id = _id_gruppo);
    IF _count = 0 THEN
        RETURN -3;
    END IF;

    IF (_username IS NULL) OR (_username = '') THEN
        RETURN -4;
    END IF;
    
    IF (_cognome IS NULL) OR (_cognome = '') THEN
        RETURN -5;
    END IF;

    IF (_nome IS NULL) OR (_nome = '') THEN
        RETURN -6;
    END IF;

    ...

Se scrivi il codice in questa maniera, puoi inserire o rimuovere un parametro (e i relativi controlli) senza alterare il resto del codice, cosa che non avverrebbe se tu concatenassi le istruzioni if. Perderai un po’ di velocità di esecuzione, ma il codice sarà molto più facile da scrivere o da modificare.
Attento, però: mettere in sequenza delle semplici istruzioni if è cosa ben diversa dal creare una catena di istruzioni else-if perché, se in caso di errore non blocchi l’elaborazione con un’istruzione return, il programma andrà avanti verificando le condizioni seguenti e l’errore nella prima condizione potrebbe ripercuotersi sul codice successivo:

/** Qui comincia il male.. **/
if ( divisore == 0 ) {
    cout << "Errore: divisione per zero" << endl;
}

/** ..e il peggio lo segue **/
if ( (dividendo / divisore) > 1 ) {
    ...
}

Non avendo un’istruzione return il codice della prima verifica non bloccherà l’esecuzione della funzione, che andrà in errore quando proverà a eseguire una divisione per zero.


L’istruzione switch permette di gestire più casi, basandosi sulla valutazione di una espressione:

switch(<espressione>)
{
    case <costante> : istruzioni... [break];
    case <costante> : istruzioni... [break];
    ...
    default: istruzioni...
}

Le parole-chiave case e default identificano i valori gestiti dall’istruzione switch. I case possono (ed è utile che siano) più di uno, ma le costanti associate a ciascuno di essi devono avere dei valori diversi. La condizione default, al contrario, deve essere unica.
L’esecuzione dell’istruzione inizia al case la cui costante è uguale al valore dell’espressione di switch e termina alla parola chiave break. Se l’espressione ha un valore non previsto dai case, l’istruzione esegue il codice associato all’etichetta default:

#include <iostream>
#include <cstdlib>

#define POS_NESSUNO -1
#define POS_ERRORE   0
#define POS_MERCURIO 1
#define POS_VENERE   2
#define POS_TERRA    3
#define POS_MARTE    4
#define POS_GIOVE    5
#define POS_SATURNO  6
#define POS_URANO    7
#define POS_NETTUNO  8
#define POS_PLUTONE  9

using namespace std;

int main(int argc, char** argv)
{    
    int pianeta;
    
    /** 
     * Legge i parametri di input.
     * Se ce ne sono, prova a convertire il primo parametro 
     * in un intero. Se il parametro non è un intero, la 
     * funzione atoi torna 0. 
     */
    if(argc > 1) {
        pianeta = atoi(argv[1]);
    } else {
        pianeta = POS_NESSUNO;
    }
    
    /** Gestisce i casi possibili */
    switch( pianeta ) {
        case POS_ERRORE:   cout << "Valore non valido"; 
            break;
        case POS_MERCURIO: cout << "Mercurio";          
            break;
        case POS_VENERE:   cout << "Venere";            
            break;
        case POS_TERRA:    cout << "Terra";             
            break;
        case POS_MARTE:    cout << "Marte";             
            break;
        case POS_GIOVE:    cout << "Giove";             
            break;
        case POS_SATURNO:  cout << "Saturno";           
            break;
        case POS_URANO:    cout << "Urano";             
            break;          
        case POS_NETTUNO:  cout << "Nettuno";           
            break;          
        case POS_PLUTONE:  cout << "Plutone";           
            break;
        default:
            cout << "Inserire un valore da: " 
                 << POS_MERCURIO 
                 << " a " 
                 << POS_PLUTONE;                    
    }
    
    cout << endl;
    
    return 0;
}

Compilando ed eseguendo questo codice, otterrai:

% g++ src/cpp/istruzioni-condizionali-switch.cpp -o src/out/esempio
% src/out/esempio                                                  
Inserire un valore da: 1 a 9
% src/out/esempio 4
Marte
% src/out/esempio terra
Valore non valido

In sostanza: con una break alla fine di ciascun caso, l’istruzione switch è una forma più elegante (ed efficiente) dell’istruzione if - else if:

if(piaeta == POS_ERRORE) {
   cout << "Valore non valido"; 
} else if(pianeta == POS_MERCURIO) {
    cout << "Mercurio"; 
} else if(pianeta == POS_VENERE) {
    cout << "Venere";   
} else if ..         

Se tu togliessi le interruzioni break alla fine di ciascun caso, l’output del programma sarebbe:

% src/out/esempio 4    
MarteGioveSaturnoUranoNettunoPlutoneInserire un valore da: 1 a 9

che in questo caso non ha senso, ma che può essere la scelta adatta se due casi possibili vanno elaborati nella stessa maniera.
Un’ultima cosa: ricordati sempre che, per dichiarare delle variabili all’interno dei case, è necessario aggiungere delle parentesi graffe; altrimenti, avrai un errore in fase di compilazione:

switch( x ) {
    case 1: 
        int y = 9;      /** errore di complilazione */
        cout << x + y; 
        break;
    case 2: {
        int y = 2;      /** corretto */
        cout << x + y ; 
        break;
    }
    default:
        cout << "default" << endl; 
}


La vita ci chiede spesso di fare delle scelte condizionali. Quando ciò avviene, hai due possibilità: o fai la scelta più conveniente per te o fai la scelta che ti sembra più conveniente per il maggior numero di persone per il più lungo periodo di tempo possibile. Nel primo caso sarai un vettore di Entropia, mentre nel secondo caso sarai un paladino della Gravità.
Come sai, per il C’hi++ la scelta esatta (inteso come participio passato del verbo esigere) è la seconda: tutta la materia non è che la manifestazione di una unica Energia, quindi ha poco senso distinguerci gli uni dagli altri; dobbiamo invece ragionare come Saʿdi di Shirāz, quando dice:

Son membra d’un corpo solo i figli di Adamo, da un’unica essenza quel giorno creati. E se uno tra essi a sventura conduca il destino, per le altre membra non resterà riparo.

Cercare il proprio tornaconto personale a discapito degli altri è sbagliato. Bisogna comportarsi bene e cercare di convincere anche gli altri a fare altrettanto, perché, come recita il Mantiq al-Tayr:

tutto il male o il bene che feci, in verità lo feci solo a me stesso.

Ma come si fa a capire cosa sia bene? Ci sono casi in cui è facile dare la scelta giusta, come nel caso del maestro Zen Bokuju:

switch( stimolo ) {
    case fame:  
        azione = mangio; 
        break;
    case sonno: 
        azione = dormo; 
        break;
    case sete:  
        azione = bevo; 
        break;
}

ma altre volte ci troviamo di fronte a scelte più complesse:

Una ragazza è rimasta incinta a séguito di una violenza: può decidere di abortire?

oppure:

Un uomo, condannato per omicidio, in carcere ha ucciso altri due carcerati e una guardia: va condannato a morte o no?

Se queste domande le fai a un cattolico, lui — coerentemente con il suo Credo — ti risponderà che no, non è possibile né abortire né condannare a morte perché la vita è un dono di Dio e nessuno ce ne può privare. Se invece queste domande le poni a un Giudice, avrai risposte diverse a seconda della Nazione a cui appartiene, perché mentre sottrarre dei beni materiali è considerato un reato ovunque, esistono degli Stati in cui è permesso sottrarre a un individuo il bene più prezioso che ha.
Un tempo, i credenti mettevano al rogo gli scienziati, accusandoli di eresia; il 6 Giugno del 1945, però, la Scienza ha mostrato al Mondo il suo potere ed è diventata di fatto il nuovo Dio per milioni di persone; da allora, le parti si sono invertite e adesso sono gli scienziati a mettere al rogo ogni forma di spiritualità. Il problema è che se privi la giurisprudenza di una base spirituale, quello che otterrai sono Leggi pret-a-porter, rimedii temporanei a delle esigenze contingenti. Nella migliore delle ipotesi.
La teocrazia è un errore, ma anche la a-teocrazia dev’essere evitata. La Fede è stata la colla che ha tenuto unita la nostra società per quasi duemila anni. Forse quella colla era solo una nostra invenzione, ma lo sono anche gli Stati, il denaro, i Diritti Umani, le Leggi. Nessuno di questi concetti così importanti per la nostra Società esiste davvero, ma li utilizziamo lo stesso perché, come il linguaggio C, pur essendo solo delle convenzioni, sono utili al loro scòpo.
Ora che questa colla non c’è più, le scelte dei legislatori non sono più mosse dal perseguimento di un obiettivo comune (corretto o sbagliato che fosse), ma dalla ricerca dell’approvazione di un elettorato composto in buona parte da zombie culturali e da egoisti che perseguono unicamente il proprio interesse momentaneo: il pascolo ideale per demagoghi con aspirazioni dittatoriali. La minoranza di idealisti e di persone colte, priva di valori trascendenti, non può che agire in base ai propri sentimenti o ai propri auspici e subisce inevitabilmente il malefico influsso dell’Annosa Dicotomia: fanno scelte che puntano al bene comune, ma si tratta di un bene comune molto spesso miope e temporaneo. Come scacchisti mediocri, vedono ciò che è bene qui e ora, ma non riescono a valutarne le conseguenze a lungo termine.
Pensa a quale potrebbe essere, secondo te, la soluzione giusta alle due domande che ti ho fatto e poi pensa al motivo quella soluzione ti appare giusta. Perché permettiamo la soppressione di un feto che non ha fatto del male a nessuno, mentre lasciamo in vita chi ne ha già fatto? Vogliamo fare la cosa giusta o vogliamo solo sentirci buoni?
Il Maestro Canaro si fece molti nemici con la sue idee sull’aborto. Anche alcune persone che inizialmente lo avevano appoggiato lo accusarono di cercare l’appoggio della Chiesa Cattolica, mentre stava solo applicando il precetto del Metta Sutra che predica la felicità non solo per tutti coloro che sono nati, ma anche per coloro che devono nascere:

bhåtà và sambhavesã và sabbe sattà bhavantu sukhitattà

bhåtà quelli che sono nati
o
sambhavesã quelli che cercano la nascita
o
sabbe tutti
sattà gli esseri
bhavantu possano essere
sukhitattà felici nel loro cuore

Tutto questo a lui non importava: quando gliene parlai, mi disse che preferiva perdere un milione di seguaci che una vita.


Rispondere alla domanda sul condannato è più difficile. Lo scòpo delle tue azioni deve essere, come sempre, il miglioramento: chi sbaglia deve capire che ha fatto un errore e non ripeterlo in altri cicli di esistenza:

int pentimento( azione ){

    bool ripetere;

    if ( azione == errore ) {
        ripetere = false;
    } else {
        ripetere = true;
    }

    return ripetere;
}

Se il peccatore è davvero pentito, allora è giusto che sia assolto, perché, come dice Attar:

Cento Mondi di peccato sono dissipati dalla luce di un solo pentimento.

Ma il pentimento dev’essere reale: il peccatore deve detestare il suo errore e scegliere di morire piuttosto che ripeterlo ancora.
Tagliare una mano a chi ruba, costringendolo a portare il cibo alla bocca con la stessa mano con cui si pulisce il sedere, è un metodo un po’ drastico, ma efficace per costringere qualcuno a meditare sull’insensatezza delle sue azioni passate — specie in un luogo come il deserto, dove i bidet sono più rari che in Francia. La Lex Talionis può funzionare per reati minori, perché chi la subisce ha il tempo di riflettere sui suoi errori, ma nel caso di un omicidio non solo è contraria all’obbligo di benevolenza che abbiamo nei confronti degli altri esseri senzienti, ma potrebbe anche essere controproducente, perché se il condannato non capisce il suo errore prima di morire è possibile che le sue azioni delittuose vengano ripetute in altri cicli dell’Universo.
D’altro canto, abbiamo un obbligo di benevolenza anche nei confronti degli altri carcerati e delle guardie carcerarie, quindi non possiamo lasciare che il condannato li uccida. La soluzione ideale sarebbe quella di metterlo in condizione di non nuocere a terzi, lasciandolo poi meditare sui suoi errori, ma se questo non fosse possibile, come ci dovremmo comportare? Se un individuo ripete più volte lo stesso atto delittuoso evidentemente non capisce o non vuole capire il suo errore. Se non capisce non è senziente, nel senso di sensibilità, quindi non può concorrere al miglioramento dell’Universo. Se non è utile al miglioramento, possiamo considerarlo alla stessa stregua del gatto di Nansen:

Nansen un giorno vide i monaci delle sale Orientali e Occidentali che litigavano per un gatto. Egli sollevò il gatto e disse: “Se mi direte una parola di Zen, salverò il gatto; se no, lo ucciderò”.
Nessuno seppe rispondere e Nansen tagliò il gatto in due.

La morte del condannato, però, se mai dovesse rendersi necessaria, non deve essere considerata una vendetta di cui gioire, ma un evento tanto doloroso quanto inevitabile, di cui dolersi come di un’amputazione. Ciascuno, in quel giorno, dovrebbe chiedersi se, con pensieri, parole, opere e omissioni, non abbia contribuito in qualche modo a quella perdita. Una Società che esalta l’individualismo, il successo e il denaro non può dirsi del tutto innocente se chi non ha i mezzi o la capacità di ottenerli in maniera lecita cerca di procurarseli in altro modo.


Un insegnante buddhista, saputo che il Maestro Canaro, nei suoi scritti, sosteneva che non c’è modo di sottrarsi al ciclo delle rinascite, si recò da lui e, deciso a dimostrare che si sbagliava, lo sfidò a un Dharma Combat per chiarire le reciproche posizioni. Il Maestro Canaro rispose che non sapeva cosa fosse un Dharma Combat; al che, l’insegnante buddhista spiegò che era un confronto dialettico, per dimostrare la propria conoscenza della dottrina. Il Maestro Canaro allora annuì e disse: “Va bene, ma prima che cominciamo, dimmi se tu, questo confronto, lo vuoi vincere o perdere, in modo che io possa accontentarti.” Sentendo quelle parole, l’insegnante buddhista si rese conto che le sue intenzioni non erano pure: non voleva quella sfida per arrivare alla verità, ma solo per il piacere della vittoria. Così si inchinò, ringraziò il Maestro Canaro per avergli fatto capire quella sua debolezza e, da quel momento in poi, divenne un suo discepolo.