Il polimorfismo

When Me they fly, I am the wings
I am the double and the int

Come avrai certamente intuito da tutto ciò che abbiamo detto finora, la caratteristica principale del C++ è il polimorfismo.

Avevamo iniziato a parlarne durante la lezione introduttiva sul C++ e l’avevamo illustrato con un esempio che, a questo punto, non dovrebbe più avere segreti, per te:

/** 
 *  @file src/cplusplus-template.cpp
 *  Esempio utlizzo dei template di classi.
 */

#include <iostream>
#include <ctime>
#include <cstring>
#include <list>

using namespace std;

/** Definisce due nuovi tipi di dato */
typedef time_t Data;
typedef enum _sesso {
    maschio = 'm',
    femmina = 'f'
} Sesso;

/** Definisce una classe base per la gestione degli animali */
class Animale {
private:
    string _razza;
    Sesso  _sesso;
public:
    /** Costruttori di copia e parametrico */
    Animale() {}
    Animale(const char* razza, const Sesso sesso ) {
        _razza  = razza;
        _sesso  = sesso;
    }
    /** Funzione virtuale pura: rende la classe "astratta" */
    virtual const char* getSpecie() const {
        return ""; 
    } 
    /** Funzioni di interfaccia */
    const char getSesso() const {        
        return (char)_sesso;
    } 
    const char* getRazza() const {
        return _razza.c_str();
    }
};

/** Operatore di output su stream per la classe Animale */
ostream& operator << (ostream& os, const Animale& animale) {
    os  << "Specie:" << animale.getSpecie() << "\t"
        << "Razza:"  << animale.getRazza()  << "\t"
        << "Sesso:"  << animale.getSesso()  
        << endl;
    return os;   
}

/** Definizione della classe derivata Cavallo */
class Cavallo : public Animale {
public:
    /** Definizione dei costruttori della classe */
    Cavallo() {}
    Cavallo(const char* razza, const Sesso sesso )
    : Animale(razza, sesso ) { 
    }
    /** Ridefinizione della funzione virtuale pura */
    const char* getSpecie() const {
        return "Cavallo"; 
    }     
};

/** Definizione della classe derivata Cavallo */
class Asino : public Animale {
public:
    /** Definizione del costruttore della classe */
    Asino() {}
    Asino(const char* razza, const Sesso sesso )
    : Animale(razza, sesso ) { 
    }
    /** Ridefinizione della funzione virtuale pura */
    const char* getSpecie() const {
        return "Asino"; 
    }     
};

/** Definizione della classe Monta */
class Monta {
private:
    Animale* _maschio;
    Animale* _femmina;
    Data     _giorno;
    string   _esito;
    /** 
    *   Funzione privata per la definizione 
    *   dell'esito della monta 
    */
    void setEsito() {
        if(strcmp(_maschio->getSpecie(),"Asino") == 0) {
            if(strcmp(_femmina->getSpecie(),"Asino") == 0) {
                _esito = "asino";
            } else {
                _esito = "mulo";
            } 
        } else {        
            if(strcmp(_femmina->getSpecie(),"Cavallo") == 0) {
                _esito = "puledro";
            } else {
                _esito = "bardotto";
            } 
        }
    }
public:
    /** Costruttore della classe */
    Monta(Animale* maschio, Animale* femmina) {
        _maschio = maschio;
        _femmina = femmina;
        time(&_giorno);
        setEsito();
    }
    /** Operatore di output, "friend" della classe Monta */
    friend ostream& operator<<(ostream& os, const Monta& copula) {
        os << "DATA:    " << asctime(localtime(&copula._giorno)) 
           << "MASCHIO: " << *copula._maschio 
           << "FEMMINA: " << *copula._femmina
           << "ESITO:   " << copula._esito
           << endl;
           return os;   
     };
};

int main()
{
    /** 
     *  Crea gli oggetti di classe derivata  
     *  e li assegna a puntatori della classe base.
     */
    Animale* cavallo  = new Cavallo("lipizzano", maschio);    
    Animale* giumenta = new Cavallo("maremmano", femmina);    
    Animale* asino    = new Asino("amiatino", maschio);
    Animale* asina    = new Asino("sardo", femmina);

    /** Crea una lista con una classe template */
    list<Monta> monte;

    /** Associa alla lista degli oggetti di classe Monta */
    monte.push_back(Monta(cavallo, giumenta)); 
    monte.push_back(Monta (asino, asina));       
    monte.push_back(Monta (asino, giumenta));     
    monte.push_back(Monta (cavallo, asina));

    /** Mostra il contenuto della lista */
    list<Monta>::iterator it;
    for (it=monte.begin(); it!=monte.end(); it++) {
        cout << *it << endl;
    }

    return 0;               
}

L’output di questo codice, nel caso l’avessi scordato, è:

> g++ src/cpp/cplusplus-template.cpp -o src/out/esempio
> src/out/esempio
DATA:    Sun May 23 14:42:51 2021
MASCHIO: Specie:Cavallo Razza:lipizzano Sesso:m
FEMMINA: Specie:Cavallo Razza:maremmano Sesso:f
ESITO:   puledro

DATA:    Sun May 23 14:42:51 2021
MASCHIO: Specie:Asino   Razza:amiatino  Sesso:m
FEMMINA: Specie:Asino   Razza:sardo     Sesso:f
ESITO:   asino

DATA:    Sun May 23 14:42:51 2021
MASCHIO: Specie:Asino   Razza:amiatino  Sesso:m
FEMMINA: Specie:Cavallo Razza:maremmano Sesso:f
ESITO:   mulo

DATA:    Sun May 23 14:42:51 2021
MASCHIO: Specie:Cavallo Razza:lipizzano Sesso:m
FEMMINA: Specie:Asino   Razza:sardo     Sesso:f
ESITO:   bardotto

Prima di andare avanti, però, è necessario fare un po’ di chiarezza su tre termini legati al polimorfismo: overload, override e ridefinizione.
Con il termine: overload di una funzione si intende la una funzione che abbia lo stesso nome di un’altra, ma dei parametri differenti. Un tipico esempio di function overload sono le differenti versioni del costruttore di una classe:

Cavallo() {}
Cavallo(const char* razza, const Sesso sesso )
: Animale(razza, sesso ) { 
}

Le due funzioni hanno lo stesso nome e il compilatore sceglierà l’una o l’altra in base ai parametri che vengono utilizzati.
Una funzione overridden è una funzione che ha una definizione diversa da quella di una funzione virtuale di una sua classe-base:

const char* getSpecie() const {
    return "Asino"; 
}     

Come abbiamo visto, il compilatore sceglie l’una o l’altra in base al tipo di oggetto utilizzato per la chiamata.
Se la funzione della classe base non fosse stata virtuale, questa sarebbe stata una semplice ridefinizione:

class Persona {
public:
    void getClass(){
        cout << "Persona" << endl;
    }
};

Quando gestisce queste funzioni, il compilatore non fa un controllo di tipo dinamico, basato sul tipo dell’oggetto al momento dell’esecuzione, ma sceglie la funzione da chiamare in base al tipo di puntatore o riferimento utilizzato, cosa che, come sai, può creare dei problemi:

Madre   * ptrM = new Madre;
Persona * ptrP = ptrM ;
ptrM->getClass() ;
ptrP->getClass() ;  // chiama la funzione di Persona - ERRORE

Alla luce di tutto ciò, possiamo correggere i commenti del codice di esempio:

/** Overload dell'operatore di output su stream  */
ostream& operator << (ostream& os, const Animale& animale) {
    os  << "Specie:" << animale.getSpecie() << "\t"
        << "Razza:"  << animale.getRazza()  << "\t"
        << "Sesso:"  << animale.getSesso()  
        << endl;
    return os;   
}

/** Override della funzione virtuale pura */
const char* getSpecie() const {
    return "Cavallo"; 
}  

/** Override della funzione virtuale pura */
const char* getSpecie() const {
    return "Asino"; 
}     

/** Overload dell'operatore di output su stream */
friend ostream& operator << (ostream& os, const Monta& copula) {
    os << "DATA:    " << asctime(localtime(&copula._giorno)) 
       << "MASCHIO: " << *copula._maschio 
       << "FEMMINA: " << *copula._femmina
       << "ESITO:   " << copula._esito
       << endl;
       return os;   
 };

Nel C++, a ogni operatore corrisponde una funzione. Quella dell’operatore binario +=, per esempio, è:

<tipo>& operator +=  (<tipo>& a, <tipo>& b) ;

laddove a e b sono i due oggetti che intervengono nell’operazione e <tipo> è il tipo delle variabili che intervengono nell’operazione.

int&    operator +=  (int&    a, int&    b) ;
float&  operator +=  (float&  a, float&  b) ;
double& operator +=  (double& a, double& b) ;

Dato che gli operatori unari possono essere prefissi o postfissi, per consentire al compilatore di distinguere le funzione corretta da utilizzare, alla funzione dell’operatore postfisso si aggiunge un secondo parametro, non utilizzato:

void operator ++ (<tipo> a) ;           // versione prefissa
void operator ++ (<tipo> a, <tipo>) ;   // versione postfissa

Le funzioni degli operatori overloaded possono essere richiamate in maniera diretta. Le due istruzioni qui sotto, una volta compilate, producono il medesimo codice e lo stesso risultato. Se riesci a trovare una qualunque ragione per usare la prima sintassi piuttosto che la seconda, fallo pure:

a = b.operator + (c) ;  
a = b + c ;

Il comportamento degli operatori è predefinito per tutti i tipi standard e può essere ridefinito per gestire anche dei tipi di dato aggregati come le strutture o le classi. La classe string, della libreria standard del C++, per esempio, ridefinisce, fra le altre cose, il comportamento degli operatori di assegnazione += e + e dell’operatore di output su stream << in modo che si possano compiere delle operazioni sulle stringhe con la stessa sintassi che si utilizza per altri tipi di dato:

/** 
 * @file src/polimorfismo-operatori.cpp
 * Esempio di overload di un operatore.
 */

#include <iostream>
#include <string>

using namespace std;

int main ()
{
    string s1 ("Pip");
    string s2 ("po");
    const char* s3 = "Plut";

    /** 
     *  La classe string definisce tre overload 
     *  per l'operatore += .
     *
     *  string& operator+= (const string& str);
     *  string& operator+= (const char* s);
     *  string& operator+= (char c);
     */
    s1 += s2;
    s1 += s3;

    cout << (s1 + 'o') << endl;

    return 0;
}

L’output di questo codice è ben noto:

> g++ src/cpp/polimorfismo-operatori.cpp -o src/out/esempio
> src/out/esempio                                          
PippoPluto

Lo stesso risultato si può ottenere anche con la funzione append:

string& append (const string& str)

ma utilizzare un operatore standard rende il codice più facile da leggere e da scrivere, se non altro perché non ti devi ricordare come si chiama la funzione per unire due stringhe.
Gli unici operatori che non possono essere ridefiniti da una classe sono:

  • l’operatore di selezione .;
  • l’operatore di risoluzione di indirizzamento dei puntatori a membri della classe .*;
  • l’operatore di risoluzione del campo d’azione ::;
  • l’operatore condizionale ? :;
  • i simboli # e ##, che vengono utilizzati dal preprocessore.

Tranne alcune eccezioni che vedremo fra poco, tutti gli operatori del C++ possono essere ridefiniti o come funzione membro di una classe o come funzione globale:

/** 
 * @file src/polimorfismo-in-out.cpp
 * Operatori come funzioni membro o globali.
 */

#include <iostream>
#include <string>

using namespace std;

struct A
{
    int _a;
    A(int a) : _a(a) {}
};

struct B
{
    int _b;
    B(int b) : _b(b) {};   
    /** Overload come funzione membro */
    int operator + (const A& a) {
        return _b + a._a;
    }
};

/** Overload come funzione globale */
int operator + (const A& a, const B& b) { return a._a + b._b; }

int main ()
{
    struct A a(3);
    struct B b(5);
    
    cout << (a + (b + a)) << endl;

    return 0;
}

Quando si ridefinisce il comportamento di un operatore per una classe, bisogna tenere conto della visibilità dei dati membro che deve utilizzare. Se l’operatore, com’è probabile, deve gestire dei dati privati o protetti, le possibilità sono due: o sfruttare le funzioni di interfaccia della classe o dichiarare l’operatore friend della classe. Nell’esempio iniziale sono applicate entrambe le possibilità: l’operatore di output su stream per la classe Animale utilizza le funzioni di interfaccia della classe:

ostream& operator << (ostream& os, const Animale& animale) {
    os  << "Specie:" << animale.getSpecie() << "\t"
        << "Razza:"  << animale.getRazza()  << "\t"
        << "Sesso:"  << animale.getSesso()  
        << endl;
    return os;   
}

mentre l’operatore di output per la classe Monta è dichiarato come friend della classe e quindi può accedere direttamente ai dati membro dell’istanza:

friend ostream& operator << (ostream& os, const Monta& copula) {
    os << "DATA: "    << asctime(localtime(&copula._giorno)) 
       << "MASCHIO: " << *copula._maschio 
       << "FEMMINA: " << *copula._femmina;
       return os;   
};

La scelta fra l’una o l’altra possibilità dipende dal tipo di programma che devi scrivere: se punti alla velocità, scegli la seconda, che è più diretta, altrimenti scegli la prima, che sarà probabilmente più lenta in esecuzione, ma non necessiterà di riscritture in caso di modifiche alla struttura della classe. Non è possibile, però, ridefinire come funzione membro di una classe una funzione operatore che abbia come primo parametro una classe di cui non si ha il controllo (come, per esempio, la funzione operatore << che ha come primo parametro un riferimento a ostream) perché nella funzione membro questo parametro sarebbe sostituito dal parametro implicito this, che ha un altro tipo di dato, causando un errore di compilazione.
Gli operatori =, (), [] e -> non possono essere ridefiniti come funzioni globali, ma devono sempre essere implementati come funzione membro non statica di una classe Le altre regole da ricordare, in questi casi, sono:

  • l’operatore unario di assegnamento = è l’unico caso di funzione membro che non viene eredi­tata da eventuali classi figlie; se non viene ridefinito, prevede l’assegnamento membro a membro degli attributi e ha la sintassi:

      C& C::operator = (const C& origine) ;
    
  • l’operatore binario [] permette di implementare vettori di tipo particolare, mantenendo una sintassi standard e ha la forma:

      c.operator [] (n) ;
    

    dove c è un oggetto di classe C e l’indice n può essere un qualsiasi tipo di dato ;

  • per ridefinire l’operatore binario di chiamata a funzione (), va utilizzata la sintassi:

      c.operator()(p) ;
    

    dove c è sempre un oggetto di classe C e p è un elenco anche vuoto, di parametri;

  • l’operatore unario di accesso ai membri della classe -> viene interpre­tato come:

      (C.operator -> ())->m ;
    

    e ritorna o un oggetto o un puntatore a un oggetto di classe C.

Ridefinire gli operatori new e delete, il cui comportamento è strettamente le­gato all’hardware, potrebbe non essere una scelta astuta dal punto di vista della port­abilità del codice; detto ciò, se una classe ha bisogno di gestire la memoria in modo particolare, lo può fare, ma deve rispettare due regole:

  • l’operatore new deve avere il primo argomento di tipo size_t e resti­tuire un puntatore a void;
  • l’operatore delete deve essere una funzione di tipo void che abbia un primo argomento di tipo void* e un secondo argomento, facoltativo, di tipo size_t.

In C, per trasformare un int in un double si utilizzano gli operatori di cast:

long int i = 5 ;
double d = (double) i ;

Il C++ accetta questa sintassi, così come accetta che si usi malloc al posto di new, ma la sua sintassi standard, che ricorda vagamente i costruttori delle classi, prevede che il dato da convertire sia passato come parametro a una funzione con lo stesso nome del tipo in cui si vuole che avvenga la conversione :

long int i = 5 ;
double d = double(i) ;

Il compilatore del C++ ha la possibilità di convertire un qualunque tipo di dato primitivo in un altro, ma non può sapere come comportarsi con i tipi di dato definiti dall’utente; dobbiamo quindi istruirlo, così come abbiamo fatto con i costruttori e gli operatori, definendo dei cam­mini di coercizione dai tipi di dato primitivi e viceversa. Il primo caso, ovvero la trasformazione dal tipo primitivo a quello definito dall’utente, è il più semplice: di fatto si tratta di definire, laddove non ci sia già, un cos­truttore per la nuova classe che richieda dei parametri di tipo primitivo. Quando invece non esiste un costruttore da estendere, ovvero quando la coercizione è dal tipo definito dall’utente a un tipo di dato primitivo o fornito in una libreria di cui non si possiede il codice sorgente, è necessario ridefinire l’operatore di conversione ().
Immagina di aver creato un nuovo tipo di dato Frazione per la gestione dei numeri razionali. Per poterlo utilizzare in espressioni contenenti dati di tipo primi­tivo dovresti ridefinire ciascun operatore per fargli accettare dei dati di tipo misto, sia come primo che come secondo parametro:

Frazione operator + (int i, Frazione f) :
Frazione operator - (int i, Frazione f) :
Frazione operator + (double i, Frazione f) :
Frazione operator - (double i, Frazione f) :
...
Frazione operator + (Frazione f, int i) :
Frazione operator - (Frazione f, int i) :
Frazione operator + (Frazione f, double i) :
Frazione operator - (Frazione f, double i) :

Puoi risparmiarti questa seccatura ridefinendo solo il com­portamento degli operatori per la nuova classe e fornendo al compilatore dei cammini di conversione dai tipi primitivi al nuovo tipo di dato, in modo che possa trasformare i dati nel tipo appropriato, nel caso di espressioni miste:

/** 
 * @file src/polimorfismo-cast.cpp
 * Gestione della conversione esplicita.
 */

#include <iostream>

using namespace std;

class Frazione
{
private:
    
    int _num ;
    int _den ;

public:
    
    /** Costruttore con parametri interi */
    Frazione(int n, int d = 1) 
    : _num(n), _den(d) {} 
    
    /** 
     * Costruttore con parametro a virgola mobile.
     * La definizione è piuttosto complessa, te la risparmio.  
     */
    Frazione(double d) ;

    /** Overload dell'operatore di cast a intero */
    operator int () { 
        return _num / _den ; 
    }

    /** Overload dell'operatore di cast a double */
    operator double() { 
        return (double) _num / (double) _den ; 
    }

    /** Overload degli operatori di somma e sottrazione */
    friend Frazione operator+ (Frazione f1, Frazione f2); 
    friend Frazione operator- (Frazione f1, Frazione f2); 

};


L’ultima cosa di cui ti devo parlare, a proposito del polimorfismo, sono i template.
I template, nel C++, sono dei modelli che si utilizzano per definire delle funzioni o delle classi polivalenti. Se uno stesso compito può essere eseguito in maniera simile su parametri di tipo differente, invece di scrivere delle funzioni o delle classi identiche per ciascun tipo di parametro, si può scrivere una funzione o una classe template e richiamarla ogni volta con il tipo di parametro appropriato:

int    somma(int    a, int    b) { return a + b; }
float  somma(float  a, float  b) { return a + b; }
double somma(double a, double b) { return a + b; }

template <class T> 
somma(T a, T b) { return a + b; }

Quando il compilatore trova nel codice un template, sia esso la dichiarazione di una classe o una chiamata a funzione, la sostituisce con il codice corrispondente, così come avviene per le macro-istruzioni del precompilatore, ma, a differenza di quello che avviene per le macro, il tipo dei parametri del template è sottoposto a uno stretto controllo, così come il resto del codice.
Il formato per la dichiarazione di una funzione template è:

template <class identificatore> dichiarazione;
template <typename identificatore> dichiarazione;

Non c’è nessuna differenza fra la prima e la seconda forma: sia class che typename producono lo stesso effetto.
identificatore è un simbolo che identifica un determinato tipo di dato o una classe definita dall’utente. Per esempio, la sintassi di una funzione template che torna il maggiore di due parametri sarà qualcosa di simile:

template<class T>
T maggiore (T x, T y) {
    return (x > y) ? x : y;
}

In questo caso, l’identificativo del tipo è la lettera T che compare sia fra gli apici nella prima riga che fra parentesi nella seconda, ma può essere qualsiasi stringa. I parametri possono essere più di uno:

template<class C1, class C2>
T funz (C1 x, C2 y) {
...
}

e possono avere un valore di default:

template<class N = int>
T funz (N n) {
...
}

La chiamata delle funzioni template è simile a quella delle funzioni ordinarie, con l’aggiunta del tipo dei parametri che devono essere gestiti:

cout << maggiore<int>   (  9,  12) << endl;    
cout << maggiore<double>(0.4, 1.2) << endl;    
cout << maggiore<char>  ('a', 'z') << endl;    

Il prossimo esempio mostra la differenza fra una macro del precompilatore e una funzione template:

/** 
 * @file src/polimorfismo-macro-template.cpp
 * Funzioni template e macro precompilatore.
 */

#include <iostream>
#include <functional>

using namespace std;

/**
*  Definzione di una macro istruzione per il 
*  precompilatore: nessun controllo di tipo.
*/
#define MAGGIORE(a,b) ((a > b) ? a : b) 

/**
*  Definzione di una funzione template
*  che torna il maggiore fra due parametri.
*/
template<class T>
T maggiore (T x, T y) {
    return (x > y) ? x : y;
}

int main ()
{     
    int    a = 10;
    short  b = 0;   
    double d = 3.123456789;
    
    /** Utilizzo della macro */
    cout << MAGGIORE(9,12)     << endl;    
    cout << MAGGIORE(0.4, 0.7) << endl;    
    cout << MAGGIORE('a', 'z') << endl;    

    /** 
    *  La stessa funzione si può utilizzare 
    *  con tipi di dato diversi:
    */
    cout << maggiore<int>   (  9,  12) << endl;    
    cout << maggiore<double>(0.4, 1.2) << endl;    
    cout << maggiore<char>  ('a', 'z') << endl;    
   
    /** Errore: confronta un carattere con un double */
    cout << MAGGIORE('a', d) << endl;    

    /**
    *   Errore: il compilatore non sa quale
    *   dei due tipi di dato utilizzare.
    */
    cout << maggiore(a, b) << endl;    
    
    return 0;
}

La macro MAGGIORE e la funzione template maggiore eseguono la stessa operazione: confrontano i due parametri che hanno ricevuto in input e tornano il maggiore dei due. La grossa differenza fra questi due approcci è che, mentre il tipo dei parametri del template è verificato dal compilatore, la macro è una banale sostituzione che non fa alcun controllo sulle variabili che utilizza. L’istruzione:

cout << MAGGIORE('a', b) << endl; 

compara un carattere con un double e, senza dare problemi in compilazione torna il valore 97, corrispondente al codice ASCII della lettera a. Al contrario, l’istruzione:

int   a = 10;
short b = 0;
cout << maggiore(a, b) << endl;  

causa un errore di compilazione perché i due parametri sono di tipo differente:

> g++ src/cpp/polimorfismo-template.cpp -o src/out/esempio
src/cpp/polimorfismo-template.cpp:52:13: 
    error: no matching function for call to 'maggiore'
    cout << maggiore(a, b) << endl;    
            ^~~~~~~~
src/cpp/polimorfismo-template.cpp:22:3: 
    note: candidate template ignored: 
        deduced conflicting types for parameter 'T'
      ('int' vs. 'short')
T maggiore (T x, T y) {
  ^

La dichiarazione di una classe template ha la forma:

template <class identificatore> dichiarazione;

La lista dei parametri fra i simboli <> può contenere uno o più simboli per i tipi dato gestiti dalla classe. L’utilizzo di queste classi è simile a quello delle funzioni template:

/** 
 * @file src/polimorfismo-classe-template.cpp
 * Esempio di classe template.
 */

#include <iostream>

using namespace std;

/**
*   Definisce una classe che gestisce coppie 
*   di coordinate.
*/
template<class T>
class Coord {
private:
    /** Dati membro con tipo variabile */
    T _x, _y; 
public:
    /** Costruttore con tipo di parametri variabile */
    Coord(const T x, const T y) 
    : _x(x), _y(y) {        
    }
    friend ostream& operator << (ostream& o, const Coord& c) {
        o << c._x << ',' << c._y ;
        return o;
    }
};

int main ()
{         
    /** Istanza con coordinate geografiche */
    Coord<double> obelisco(41.903219, 12.458157);
    
    /** Istanza con coordinate schermo */
    Coord<int>    pixel(821, 134);
    
    cout << "Obelisco:" << obelisco << endl;    
    cout << "Pixel:   " << pixel    << endl;    
    
    return 0;
}


Il codice che ti ho mostrato all’inizio di questa lezione utilizza una classe template:

list<Monta> monte;

La classe list è una delle classi della Standard Template Library del C++, una libreria di classi e di funzioni che permettono di risolvere dei problemi comuni della programmazione, come la memorizzazione, l’ordinamento o la ricerca di una serie di dati. Le componenti della STL è sono:

  • una libreria di container che permettono di immagazzinare oggetti e dati;
  • degli iteratori che consentono di scorrere il contenuto dei container;
  • una collezione di algoritmi che permettono di eseguire delle operazioni di ordinamento e ricerca su insiemi di dati;
  • degli oggetti-funzioni, o: functors, che incapsulano una specifica funzione.

La classe list è un esempio di container e rappresenta un elenco di elementi memorizzati in aree non contigue della memoria. Al contrario, la classe vector implementa un elenco di elementi memorizzati in un’unica area di memoria, così come avviene per gli array del C.
Tutti i vettori della STL posseggono delle funzioni membro che consentono di gestirne gli elementi; la funzione push_back, per esempio, aggiunge un elemento in coda alla lista:

monte.push_back(Monta(cavallo, giumenta)); 
monte.push_back(Monta (asino, asina));       
monte.push_back(Monta (asino, giumenta));     
monte.push_back(Monta (cavallo, asina));

Gli iteratori sono dei costrutti che permettono di scorrere il contenuto di un container, individuandone gli elementi. Ne abbiamo utilizzato uno nell’istruzione:

list<Monta>::iterator it;
for (it=monte.begin(); it!=monte.end(); it++) {
    cout << *it << endl;
}

La prima istruzione del ciclo for assegna all’iteratore it il primo elemento della lista, tornato dalla funzione membro monte.begin. La seconda istruzione, verifica che l’iteratore sia differente da monte.end, che punta alla fine della lista. La terza istruzione incrementa l’iteratore di una posizione e dimostra come la ridefinizione di un operatore per una classe renda il codice più facile da leggere: anche se tu non hai mai visto una classe template, capisci subito che quella istruzione incrementa il valore di it di un’unità.
Gli algoritmi della STL, definiti nell’header <algorithm> sono funzioni template che permettono di individuare, copiare, ordinare, unire o eliminare i dati all’interno di un container.

/** 
 * @file src/polimorfismo-algoritmi.cpp
 * Esempio di utilizzo degli algoritmi della STL.
 */

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main()
{
    /** Crea il vettore */
    vector<int> vect;

    /** Aggiunge dei valori al vettore */
    vect.push_back(10);
    vect.push_back(70);
    vect.push_back(21);
    vect.push_back(49);
    vect.push_back(35);

    /** Mostra il valore più alto e il più basso */
    cout << *min_element(vect.begin(), vect.end()) << endl;
    cout << *max_element(vect.begin(), vect.end()) << endl;
    
    /** Mostra i valori nell'ordine in cui sono stati inseriti */
    vector<int>::iterator i;
    for(i = vect.begin(); i != vect.end(); i++) {
        cout << *i << " ";
    }
    cout << endl;

    /** Ordina i valori dal più basso al più alto e li visualizza */
    int n = vect.size();
    sort(vect.begin(), vect.end());
    for (int i=0; i<n; i++) {
        cout << vect[i] << " ";       
    }
    cout << endl;

    /** Ordina i valori dal più alto al più basso e li visualizza */
    reverse(vect.begin(), vect.end());
    for (int i=0; i<n; i++) {
        cout << vect[i] << " ";       
    }
    cout << endl;

 
    return 0;
}

Se compili ed esegui questo codice, ottieni:

> g++ src/cpp/polimorfismo-algoritmi.cpp -o src/out/esempio
> src/out/esempio                                          
10
70
10 70 21 49 35 
10 21 35 49 70 
70 49 35 21 10 

Le function-class o: functors sono delle classi che ridefiniscono il comportamento dell’operatore () e che possono quindi agire come se fossero delle funzioni:

/** 
 * @file src/polimorfismo-functor-stl.cpp
 * Esempio di function objects della STL.
 */

#include <iostream>
#include <functional>

using namespace std;

int main ()
{
    int a = 12;
    int b = 4;

    /** Dichiarazione di oggetti functor */
    plus<int>       p;
    minus<int>      m;
    multiplies<int> x;
    divides<int>    d;
    modulus<int>    o;

    /** Utilizzo degli oggetti come fossero delle funzioni */
    cout << "plus: "       << p(a,b) << endl;
    cout << "minus: "      << m(a,b) << endl;
    cout << "multiplies: " << x(a,b) << endl;
    cout << "divides: "    << d(a,b) << endl;
    cout << "modulus: "    << o(a,b) << endl;

    /** "esegue"" l'oggetto o con nuovi parametri */
    cout << "modulus: "    << o(a,5) << endl;

    return 0;
}

Utilizzati così, i functor hanno poco senso, ma possono essere (e sono) molto utili quando si utilizzano quelle funzioni della STl che elaborano tutti gli elementi di un container, come per esempio la funzione transform:

/** 
 * @file src/polimorfismo-rot13.cpp
 * Trasformazione di una stringa con una funzione.
 */

#include <string>
#include <cctype>
#include <iostream>
#include <functional>

using namespace std;

/** 
*   Funzione che converte i caratteri di una stringa in rot13  
*   Il ROT13 è un algoritmo di cifratura piuttosto banale, 
*   perché incrementa di 13 il valore di ciascun carattere. 
*   Non è un algoritmo realmente sicuro, però, perché per 
*   decifrare il testo crittografato basta crittografarlo 
*   di nuovo.  
*/
unsigned char rot13(unsigned char c) 
{ 
    unsigned char rot = c;
    if (isalpha(c)) {
        rot = ((tolower(c) - 'a') < 14) ? c + 13 : c - 13;
    }
    return rot;
}

int main ()
{
    string pp("PippoPluto"); 

    /** Elabora la stringa con la funzione transform */
    transform(
               pp.begin()   // inizio del container da modificare
             , pp.end()     // fine del container da modificare
             , pp.begin()   // container di output
             , rot13        // funzione da applicare
             );

    cout << pp << endl; 

    /** Ripetendo l'operazione, il testo torna normale. */
    transform(
               pp.begin()   
             , pp.end()     
             , pp.begin()   
             , rot13        
             );

    cout << pp << endl; 
    
    return 0;
}

Se compili ed esegui questo programma, otterrai :

> g++ src/cpp/polimorfismo-rot13.cpp -o src/out/esempio     
> ./src/out/esempio
CvccbCyhgb
PippoPluto

Le funzioni ordinarie ti permettono di sfruttare l’algoritmo transform per cifrare un testo con un valore fisso, ma non puoi fare la stessa cosa utilizzando una chiave variabile, perché il quarto parametro non accetta funzioni con più di un parametro. Se provassi a utilizzarlo con qualcosa come:

unsigned char cifra(unsigned char c, int chiave) 
{ 
    return c + chiave;
}

otterresti l’errore:

/Library/Developer/CommandLineTools/usr/bin/../include/c++/v1/algorithm:1855:34: error: too few arguments to
      function call, expected 2, have 1
        *__result = __op(*__first);
                    ~~~~         ^
src/cpp/polimorfismo-transform-chiave.cpp:25:5: note: in instantiation of function template specialization
      'std::__1::transform<std::__1::__wrap_iter<char *>, std::__1::__wrap_iter<char *>, unsigned char
      (*)(unsigned char, int)>' requested here
    transform(
    ^

È in questi casi che tornano utili i functor, perché possono essere inizializzati con uno o più valori specifici e poi essere utilizzati come funzioni unarie:

/** 
 * @file src/polimorfismo-functor.cpp
 * Creazione di una classe functor.
 */

#include <string>
#include <cctype>
#include <iostream>
#include <functional>

using namespace std;

/** Dichiarazione della classe functor */
class Cifra
{
private:

    int _chiave;

public:
    
    /** 
    *   Il costruttore della classe ha come parametro
    *   il valore della chiave di cifratura
    */
    Cifra(int chiave) : _chiave(chiave) {  }
  
    /** Ridefinizione dell'operatore () */
    unsigned char operator () (unsigned char c) const {
        return c + _chiave;
    }
};

int main ()
{
    string pp("PippoPluto"); 

    /** 
    *   Richiama transform passando come parametro
    *   un'istanza del functor, inizializzata con 
    *   la chiave di cifratura.
    */
    transform(
               pp.begin()   
             , pp.end()     
             , pp.begin()   
             , Cifra(1)        
             );

    cout << pp << endl; 
    
    return 0;
}

Compilando ed eseguendo questo programma, ottieni :

> g++ src/cpp/polimorfismo-functor.cpp -o src/out/esempio
> ./src/out/esempio
QjqqpQmvup

che corrisponde ai caratteri della stringa PippoPluto incrementati di un’unità.


Da migliaia di anni, gli uomini cercano di capire quale sia il significato dell’Esistenza.
Le risposte che si sono dati variano a seconda del periodo storico e del territorio in cui il profeta o il filosofo ha vissuto, ma hanno tutte una particolarità: richiedono ai loro seguaci l’accettazione di postulati non dimostrabili, come l’esistenza di una o più divinità o di stati di esistenza diversi da quello che conosciamo. Anche la Scienza ha provato a dare delle risposte agli stessi interrogativi, ma la sua indagine si è limitata agli aspetti pratici del problema: ha prodotto delle interessanti teorie sulla genesi dell’Universo e sugli eventi che hanno portato alla nostra esistenza, ma non si è mai pronunciata su quello che potrebbe essere il nostro ruolo in tutto ciò, con le conseguenze di cui abbiamo parlato durante la lezione sulla memoria.
Il Maestro Canaro, che non riusciva ad accettare né i dogmi delle religioni tradizionali né lo scollamento fra uomo e Universo prodotto dalle ipotesi scientifiche, si pose una domanda:

È possibile dare una spiegazione dell’Esistenza sfruttando solo ciò di cui abbiamo esperienza diretta?

La maggior parte delle religioni, per “funzionare”, richiede da una a tre dimensioni aggiuntive, oltre quelle note; la Scienza, per le sue super-stringhe ha bisogno almeno di sette dimensioni aggiuntive, ovvero il doppio di quelle che servono per un Aldilà non spirituale. Esiste una spiegazione più semplice?
Non essendo né un filosofo né un mistico, approcciò lo sviluppo della sua dottrina come se fosse stata un sistema software. Per prima cosa fece un’analisi del “sistema in esercizio”, evidenziandone i principali difetti; poi identificò delle vulnerabilità logiche delle religioni canoniche e definì delle linee-guida atte a prevenirle; infine, descrisse le caratteristiche del C’hi++, spiegando come queste avrebbero potuto risolvere alcuni dei problemi evidenziati in precedenza. Come scrisse nella Proposta, ci sono dei “bug” che possiamo considerare comuni a tutte le metafisiche:

  • i dogmi, che sono le fondamenta delle dottrine, sono facilmente attaccabili perché non possono essere dimostrati, ma solo accettati per fede;
  • una religione può avere delle difficoltà nel modificare la propria dottrina, anche quando è evidente che uno dei suoi dogmi è errato;
  • la contestazione di un dogma causa quasi inevitabilmente una separazione e le separazioni è probabile che sfocino in conflitti.

ed altri, che possiamo considerare comuni agli esseri umani:

  • la tendenza a difendere i propri principii anche con mezzi che contrastano con i principii stessi;
  • la tendenza a influenzare la propria obiettività con le proprie speranze.

Per correggere o quanto meno mitigare questi problemi, la sua metafisica avrebbe dovuto:

  • limitare il numero dei dogmi;
  • limitare gli elementi metafisici e le accettazioni per fede;
  • non proporsi come Unica Verità Incontestabile, ma come un'approssimazione sicuramente incompleta e perfettibile della Verità;
  • riconoscere le contraddizioni della dottrina e analizzarle obiettivamente, anche se ciò porterà a modificare la dottrina stessa.

Il Maestro Canaro applicò allo sviluppo della sua metafisica-non-metafisica lo stesso approccio che adottava quando doveva realizzare un software. Ci sono due modi diversi di progettare un software: il primo consiste nell’analizzare tutti i sistemi che svolgono azioni simili, prendere il meglio di ciascuno e metterlo nel nuovo sistema; in alternativa, si può progettare il sistema da zero e solo quando se ne è definita per grandi linee la struttura, studiare le soluzioni adottate dagli altri, integrandole nel proprio programma se lo si ritiene utile. Il primo approccio è più rapido e sicuro, ma tende a produrre risultati ripetitivi; il secondo approccio è più complesso, sia in termini di analisi che di implementazione, ma facilita l’innovazione perché l’immaginazione dell’analista non è condizionata da ciò che ha visto.
Essendo un sostenitore del secondo metodo, il Maestro Canaro lo applicò anche al C’hi++ e, dopo alcuni di anni di studio, arrivò alla conclusione che non solo è possibile ipotizzare una cosmogonia quasi del tutto priva di elementi metafisici (non del tutto priva, perché, come vedremo in seguito, una dose minima di trascendenza è necessaria per garantire la buona funzionalità della dottrina), ma che i precetti di questa dottrina erano compatibili con molti principii delle religioni canoniche.


Il C’hi++ ereditò alcuni concetti proprii delle filosofie note al Maestro Canaro, come il dualismo Gravità/Elettricità elaborato da Poe in Eureka, che lo aveva affascinato per il modo in cui trasformava una forza cieca e inspiegabile come la Gravità nell’intenzione, cosciente, di tutto ciò che esiste di tornare a essere Uno. D’altro canto, la dottrina del Maestro Canaro rinnegò alcuni concetti comuni a molte religioni, come la possibilità di sottrarsi al ciclo delle rinascite o la presenza di punizioni o premii ad-personam.
Così come quando si analizza il funzionamento di un software non ci si cura delle singole variabili, ma si pensa al flusso complessivo del sistema, così il C’hi++ vede l’esistenza non in termini di interazioni fra individui, ma come l’evoluzione del flusso dell’Energia dell’Uno all’interno della matrice tridimensionale degli spazioni. Per il C’hi++ non esistono né anime, né fiumi infernali e chi muore in mare non troverà ad accoglierlo Rán, nella sua birreria in fondo al mare, ma verrà semplicemente riciclato, come le aree di memoria RAM all’interno di un computer.
Le nostre esistenze sono incidentali; pensare di punirle o di premiarle non avrebbe senso e contrasterebbe con il principio generale che tutto ciò che esiste è la manifestazione di un’unica Entità. Come ti ho detto all’inizio di queste lezioni, non è possibile andare in Paradiso o all’Inferno da soli: qualunque cosa avvenga nell’Universo, ci riguarda tutti.
Questo però non vuol dire che il C’hi++ rifiuti tutti concetti delle religioni che lo hanno preceduto; anzi. Molti precetti del C’hi++ sono compatibili con precetti o idee appartenenti ad altre mistiche o filosofie e si tratta spesso di filosofie che il Maestro Canaro non conosceva, quando pose la basi della sua dottrina. Per esempio, il Maestro Canaro non lesse mai (con suo grande rammarico) la Divina Commedia; ciò non ostante, il C’hi++ ha un punto di contatto con la visione dantesca dell’Aldilà come conseguenza del pentimento. Dante mette in Purgatorio i peccatori che hanno capito di aver sbagliato, mentre condanna all’Inferno quelli che, malgrado tutto, non riescono a prendere coscienza delle proprie colpe. Come abbiamo detto in precedenza e come vedremo durante la lezione sul debug, il C’hi++ concorda con questa idea.
Similmente, ci sono diverse affinità fra i C’hi++ e la Bhagavad-Gita, anche se lui la lesse mentre stava redigendo la Proposta, quando i punti nodali del suo Credo erano già stati definiti.
Oltre alla citazione che ti ho fatto parlando del programmatore, ci sono dei brani che ricordano molto le affermazioni contenute in Sostiene Aristotele; per esempio, sulla natura dell’Universo:

Alla fine del proprio ciclo d’esistenza, un mondo collassa su se stesso, riassorbendo in una massa tenebrosa ogni forma di manifestazione: esseri viventi e oggetti inanimati giacciono allo stato latente in una condizione caotica. I cicli cosmici sono periodi temporali chiamati Manvantara, suddivisi al proprio interno in quattro ere o Yuga, ciascuna caratterizzata da una particolare qualità dell’esistenza. Si tratta di un ritorno periodico a condizioni di vita non uguali ma analoghe, da un punto di vista qualitativo, a quelle dei cicli precedenti, una successione di quattro ere che ricorda, su scala ridotta, l’alternarsi delle quattro stagioni.

O sul dualismo Gravità/Entropia :

Il Sāṁkhya, la dottrina su cui si fonda lo Yoga, parla di due princìpi che, interagendo tra loro, manifestano l’intero universo con tutti gli esseri viventi e gli oggetti inanimati che lo popolano: Prakṛti, il polo materiale e femminile, e Puruṣa, quello spirituale e maschile; nell’essere umano Prakṛti costituisce il corpo e la mente, che diventano la dimora dell’anima individuale (puruṣa).

O su quelli che lui definiva: i Post-It:

Ci sono due categorie di saṁskāra; la prima consiste nelle vāsanā, che sono impressioni lasciate nella mente dagli avvenimenti passati, tracce qui conservate allo stato latente ma pronte a manifestarsi in presenza delle condizioni adatte, cioè di situazioni analoghe a quelle che le hanno generate, e che le attiverebbero a causa della loro affinità. Sulla spinta delle vāsanā, una volta che siano attivate, e degli stati d’animo che queste manifestano, l’individuo presenta una tendenza inconscia ad agire in un determinato modo, e più in generale ad avere un certo tipo di comportamento, di sensibilità, di carattere; si tratta di una predisposizione innata che lo induce, nel bene come nel male, ad un comportamento analogo a quello che ha tenuto in passato, creando un circolo vizioso (o virtuoso) che si autoalimenta.

Puoi trovare delle analogie con i precetti del C’hi++ anche nel Mantiq al-Tayr:

Tutto è un’unica sostanza in molteplici forme, tutto è un unico discorso in diverse espressioni (…) Egli sfugge a ogni spiegazione, a qualsiasi attributo. Di Lui soltanto una pallida idea ci è concessa, dare compiuta notizia di Lui è impossibile. Per quanto bene o male si parli di Lui, in realtà d’altri non si parla che di se stessi.

o anche:

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

Per certi versi anche la stessa Genesi biblica può essere considerata un’allegoria della cosmogonia spazionista: il Paradiso è l’Uno primigenio, mentre Adamo (Puruṣa) ed Eva (Prakṛti) sono l’Ente che ne causa la disgregazione, generando un Universo dove si partorisce nel dolore e dove ci si deve guadagnare il pane con il sudore della fronte.
Il Maestro Canaro pensava che tutto questo fosse normale. Come scrisse nel MANIFEST GitHub del C’hi++:

Spogliate degli orpelli e ricondotte alle loro caratteristiche essenziali, le diverse ipotesi metafisiche hanno molti punti in comune perché sono tutte, in una maniera o nell’altra, la risposta a uno stesso bisogno: la ricerca di una giustificazione alla nostra esistenza.

In una nota della mappa mentale su cui basò lo sviluppo iniziale della dottrina, aggiunse:

Le diverse religioni, possono essere delle forme derivate di una stessa mistica iniziale? Esistono dei “dati membro” e delle funzioni comuni, che siano state ridefinite con il passare del tempo, ma che facciano capo a un corpo di credenze (o di nozioni) iniziale? Anche solo in questa mappa, se ne trovano diverse (p.es. Empedocle -> Poe). Così come le classi di un linguaggio Object-Oriented sono ridefinite per adattarsi a uno specifico contesto di utilizzo, così pure la Mistica iniziale potrebbe essere stata “overloaded” per adattarsi a uno specifico luogo o tempo. Se fosse così, tanto più si va indietro nel tempo, e quindi nella gerarchia di classi, tanto più ci si dovrebbe avvicinare alle caratteristiche proprie della Mistica. È possibile definire una gerarchia di classi figlie della classe astratta Credo? Semplificando molto (visto che sono le 3 di notte): Budda e Zoroastro influenzano i Greci, che influenzano gli Ebrei, che a loro volta influenzano i Cristiani, che alla fine producono i Testimoni di Geova… Allo stesso modo (sempre semplificando), dal C si è evoluto il C++ e dal C++, Java.

Solo alcuni anni dopo, annotò questa frase in un libro di Guenon:

Il vero spirito tradizionale, quale si sia la forma da esso rivestita, è in fondo sempre e ovunque lo stesso; le forme diverse, specificamente adatte a queste o quelle condizioni mentali, a queste o quelle circostanze di tempo e di luogo, sono solo le espressioni di una unica e sola verità.


Fra il C’hi++ e le religioni canoniche c’è la stessa differenza che passa fra una mappa topografica e un’immagine da satellite.
Quel senza Dio di Dawkins, ha detto che:

Uno dei caratteri di una folle stravaganza è un uso troppo entusiasta dell’analogia.

Una frase curiosa, da parte di un esponente di una setta che cerca di descrivere tutto ciò che esiste con analogie matematiche e nega l’esistenza di ciò che non riesce a convertire..
Entusiasmi a parte, le mappe e le immagini da satellite hanno diverse analogie con le discipline metafisiche. Anche le mappe e le immagini, come la metafisica, sono costrette a rappresentare il loro soggetto a un rapporto di scala ridotto e con due sole dimensioni in vece di tre (o di quattro se, oltre alla profondità, vuoi considerare anche il tempo). Anche le mappe e le immagini, per questo motivo, devono rappresentare il loro soggetto per mezzo di analogie: le carte topografiche usano delle linee altimetriche e dei simboli; le immagini satellitari usano dei pixel o dei piccoli punti di colore. In nessuno dei due casi ciò che noi vediamo è davvero ciò che rappresenta; è il nostro cervello che decide di crederlo tale: nel caso della carta topografica, perché la legenda ci permette di definire una correlazione fra significato e significante; nel caso dell’immagine, perché il nostro occhio riconosce in quelle combinazioni di pixel o di punti di colore degli alberi, il mare o delle case.
Un’altra analogia, conseguenza dei due punti precedenti, è che è sbagliato confondere i simboli con ciò che rappresentano: i quadratini scuri delle mappe non sono case; i punti colorati delle immagini non sono un bosco. Mappe e immagini hanno senso solo a un certo livello di lettura; se lo oltrepassiamo, se cerchiamo di ottenere più informazioni o verosimiglianza avvicinando lo sguardo, otteniamo l’effetto opposto, perché i simboli si rivelano per quello che sono: punti colorati o linee su un foglio. Questo però non vuol dire che ciò che rappresentano sia falso, ma che noi non stiamo guardando con il giusto paio di occhi, come direbbe Hunter Thompson.
Il Maestro Canaro pensava che fosse per questo motivo che alcune religioni sono contrarie alla rappresentazione diretta della Divinità: perché è facile che poi si confonda il simbolo con ciò che rappresenta. Tornando al paragone iniziale, le religioni tradizionali sono delle immagini da satellite, mentre il C’hi++ è una mappa topografica.
Mentre i Credi religiosi riescono a riprodurre — nei limiti imposti dalla nostra condizione — tutta la bellezza del Creato, il C’hi++ si limita a darne una descrizione schematica, più povera di contenuti e di poesia, ma più facile da accettare per chi non abbia la benedizione della Fede. Un’immagine da satellite ha un valore contemplativo: è bella da guardare sullo schermo del tuo computer o anche da appendere al muro, come un quadro, ma se ti sei perso in un bosco o in mezzo ai monti, una mappa topografica, proprio in virtù della sua schematicità, ti permetterà più facilmente di ritrovare la strada di casa.
Il C’hi++ non cerca di rubare fedeli alle religioni canoniche. Non avrebbe senso: sarebbe come cercare di convincere chi sia già sposato con l’amore della sua vita a fare un matrimonio di interesse: se tu hai la Fede non hai bisogno di conferme razionali; possono compiacerti, ma non ti sono necessarie. Il C’hi++, però, può dare forza a quelle (tante) persone che ancora credono in tutto ciò in cui più nessuno crede, come li descrisse Longanesi; quella Banda degli Onesti che tutti i giorni fa il proprio dovere al meglio possibile anche se non gli conviene, anche tutto e tutti intorno a loro sembrano spingerli all’egoismo e all’indifferenza. Può aiutarli a non arrendersi e può insegnare loro che non è importante vincere le partite, ma giocare sempre meglio. Riconoscere gli sbagli che si sono fatti, imparare da essi e cercare di non ripeterli più, partita dopo partita, in una ricerca continua del meglio. Se si comporteranno così, qualunque sarà il loro lavoro, fosse anche pulire i cessi, sarà comunque Arte.