Programmazione Scientifica++: Lezione 13

Membri e Metodi Statici

Docente

Argomenti di Oggi

  • Membri Dati static
    • Condividere informazioni tra oggetti
  • Metodi static
    • Funzioni di classe e Utility Classes
  • I Limiti dell’Overloading Interno
    • L’asimmetria matematica
  • Funzioni Globali
    • Estendere gli operatori dall’esterno
  • La direttiva friend
    • Concedere permessi speciali

I Limiti degli Attributi Normali

Fino ad ora, abbiamo imparato che il C++ si basa sull’Incapsulamento: ogni oggetto è una scatola chiusa con i propri dati. Se definiamo una classe Particella con un attributo massa, ogni singola particella creata avrà il suo personale valore di massa.

class Particella {
public:
    double massa_;
};

int main() {
    Particella p1; p1.massa_ = 5.0;
1    Particella p2; p2.massa_ = 10.0;
}
1
p1 e p2 sono entità separate in RAM. Modificare p2 non ha alcun effetto su p1.

Oggetti come Isole

Questa indipendenza è il punto di forza della programmazione orientata agli oggetti (OOP). Le istanze sono “isole” che non interferiscono tra loro.

Ma cosa succede se abbiamo un’informazione che riguarda tutti gli oggetti contemporaneamente?

Immaginate di voler tenere traccia di quante istanze di Particella sono state create nel corso del vostro programma.

Un attributo di classe come int totale_particelle; non funzionerebbe! Essendo un normale attributo, ogni particella avrebbe il suo contatore personale (partente da 0), che non sa nulla delle altre particelle.

La Soluzione Ingenua (Variabile Globale)

I programmatori alle prime armi (che spesso provengono dal C standard) tendono a risolvere questo problema dichiarando una variabile globale fuori da tutto.

1int TOTALE_PARTICELLE = 0;

class Particella {
public:
    Particella() {
        TOTALE_PARTICELLE++; // Incrementiamo la variabile globale
    }
    ~Particella() {
        TOTALE_PARTICELLE--; // Decrementiamo alla distruzione
    }
};
1
Questa variabile vive all’aperto, fuori da qualsiasi controllo.

Perché le variabili globali sono il Male

Usare variabili globali in C++ (e in qualsiasi linguaggio moderno) è considerata una pratica disastrosa.

Warning

  1. Nessun Incapsulamento: Qualsiasi funzione, ovunque nel programma, può fare TOTALE_PARTICELLE = -999; e distruggere la logica della vostra applicazione.
  2. Inquinamento del Namespace: Se usate librerie esterne che usano a loro volta una variabile globale con lo stesso nome… il programma non compilerà.

Vogliamo una variabile globale, sì, ma protetta e confinata all’interno della nostra classe.

La Soluzione Reale: Membri static

Il C++ introduce la parola chiave static per i membri di una classe.

Se dichiari un attributo come static, stai dicendo al compilatore: “Non creare una copia di questa variabile per ogni oggetto. Creane una sola in assoluto, condivisa da tutte le istanze di questa classe!”.

class Particella {
private:
    // Questa è CONDIVISA da tutte le particelle.
1    static int totale_particelle_;
    
    // Questa è UNICA per ogni particella.
    double massa_; 
};
1
Essendo private, nessuno fuori dalla classe può manomettere il contatore!

Membri static: Dichiarazione (.hpp)

Le variabili static richiedono una cura particolare: devono essere separate tra file Header e file Source.

Nel file .hpp (l’Interfaccia) ci limitiamo a dichiarare che la variabile esisterà.

Particella.hpp

#pragma once

class Particella {
private:
    static int totale_particelle_; // Dichiaro l'esistenza
    
public:
    Particella();
    ~Particella();
};

Membri static: Inizializzazione (.cpp)

Nel file .cpp (l’Implementazione), la variabile statica deve essere inizializzata. E deve essere fatto fuori da qualsiasi costruttore! Questo perché la variabile statica esiste ancor prima che il primo oggetto venga creato!

Particella.cpp

#include "Particella.hpp"

// INIZIALIZZAZIONE DELLA VARIABILE STATICA
// Si fa usando il nome della classe e l'operatore di scope ::
// Nota: NON devi rimettere la keyword "static" qui!
1int Particella::totale_particelle_ = 0;

Particella::Particella() {
2    totale_particelle_++;
}

Particella::~Particella() {
3    totale_particelle_--;
}
1
Inizializzata in RAM prima ancora che parta il main().
2
Ogni volta che nasce un oggetto, il contatore sale.
3
Ogni volta che muore, scende.

Membri static: Un esempio completo

Rivediamo cosa accade in RAM se eseguiamo questo codice:

int main() {
    // totale_particelle_ in memoria vale 0
    
    Particella p1; // totale_particelle_ diventa 1
    Particella p2; // totale_particelle_ diventa 2
    
    { // Apro uno scope limitato
        Particella p3; // totale_particelle_ diventa 3
    } // p3 muore qui! totale_particelle_ diventa 2
    
    // totale_particelle_ vale 2!
    
    return 0;
}

È un meccanismo perfetto per tenere traccia di ID, contatori, risorse condivise (come la connessione a un Database o un File di Log).

La Domanda Fondamentale

Tutto fantastico. Abbiamo incapsulato la nostra variabile statica mettendola private.

Ma ora… come facciamo a leggerla? Dovremo scrivere un Getter!

class Particella {
public:
1    int getTotale() const { return totale_particelle_; }
};
1
Questo metodo appartiene agli oggetti. Funziona.

Ma aspetta! Se non ho creato nemmeno un oggetto, come faccio a leggere il totale (che dovrebbe essere 0)? Non ho un oggetto su cui chiamare getTotale()!

Metodi static (Le funzioni di Classe)

Per risolvere questo impasse, il C++ ci permette di dichiarare static anche le Funzioni Membro (i metodi).

Un metodo static non appartiene ai singoli oggetti, appartiene all’Intera Classe. Può essere invocato anche se non esistono istanze.

class Particella {
private:
    static int totale_particelle_;
    
public:
    // Metodo STATICO!
1    static int getTotale() { return totale_particelle_; }
};
1
Attenzione: non può essere marcato const alla fine. Approfondiremo nella prossima slide il perché.

Invocare un metodo static

Per invocare un metodo statico nel main, non usiamo il punto . su un oggetto. Usiamo l’operatore di scope :: direttamente sul nome della classe!

int main() {
    // Non ho creato nessuna particella!
    
    // Invocazione statica
1    std::cout << "Totale: " << Particella::getTotale();
    
    // Ora creo un oggetto
    Particella p1;
    
    // L'output ora sarà 1
    std::cout << "Totale: " << Particella::getTotale();
    
    return 0;
}
1
Output: 0. Elegante e funzionale!

Cosa NON puoi fare in un metodo static

I metodi static vivono nel “vuoto cosmico” dell’astrazione della classe. Non sono agganciati a un oggetto fisico. Pertanto:

Important

  1. Nessun this: Non esiste il puntatore this in un metodo statico, perché non c’è nessun “oggetto corrente”.
  2. Niente dati normali: Un metodo statico non può leggere le variabili normali della classe (es. massa_). Quale massa dovrebbe leggere, se non c’è l’oggetto?
  3. Niente const finale: Come visto prima, il const alla fine di un metodo serve a proteggere l’oggetto fisico da modifiche. Non essendoci l’oggetto fisico, mettere const non ha senso (è un errore di compilazione).

I metodi statici possono accedere SOLO a variabili statiche!

Un Caso d’Uso Reale: Le Utility Classes

In ambiti scientifici e di Machine Learning, i metodi statici non si usano solo per i contatori. Si usano per creare le cosiddette Utility Classes.

Immagina di dover applicare la funzione di attivazione Sigmoide a un neurone. La formula è universale: \(f(x) = \frac{1}{1 + e^{-x}}\)

Dovremmo per forza creare un oggetto Attivazione per usarla?

Attivazione att;   // Spreco di RAM!
double y = att.sigmoide(2.5);

Non ha senso. La sigmoide è logica matematica pura, non ha uno “stato”.

Utility Classes nel Machine Learning

Raggruppiamo tutte le funzioni matematiche in una Classe che contiene SOLO metodi statici!

#include <cmath>

class MathUtils {
public:
    static double relu(double x) {
        return (x > 0) ? x : 0.0;
    }
    
    static double sigmoid(double x) {
        return 1.0 / (1.0 + std::exp(-x));
    }
};

int main() {
    // Uso le funzioni come fossero una libreria matematica!
1    double y = MathUtils::relu(-5.0);
}
1
Questa è la base su cui sono costruite gran parte delle librerie professionali C++.