Dal C al C++/Utilizzo avanzato di librerie/Ridefinizione di funzioni membro

Supponiamo di avere adottato una libreria, sviluppata da un'altra organizzazione, per creare interfacce-utente grafiche. In tale libreria, c'è la classe CampoValuta, le cui istanze gestiscono un widget (finestrella figlia posizionata all'interno di una finestra di dialogo) al fine di mostrare all'utente importi monetari, e di permettergli di modificarli.

Usando tale classe nel modo più semplice, i valori vengono mostrati sempre in testo nero su sfondo bianco. Per un'esigenza applicativa, si vuole invece che il testo sia mostrato in blu.

A tale scopo, l'organizzazione che ha sviluppato la libreria ha ritenuto utile aggiungere la funzione membro "void imposta_colore_testo(Colore colore_testo)", dove "Colore" è un tipo che si suppone definito in precedenza dalla libreria stessa. Chiamando questa funzione su un oggetto di tipo "CampoValuta", si modifica il colore usato dal widget associato a tale oggetto per mostrare il testo.

In tal modo, la maggior parte degli utenti della libreria, per cui il colore di default è adeguato, non devono impostare nessun colore, ma chi avesse esigenze particolari può scegliere il colore che preferisce.

Tuttavia, questa soluzione ha un inconveniente. Noi stiamo sviluppando un'applicazione finanziaria, in cui ci sono parecchie decine di importi valutari, e vogliamo che tutti abbiano lo stesso colore. Quindi, per ognuno dei campi valutari dobbiamo chiamare la stessa funzione con gli stessi parametri, il che è ovviamente scomodo. Inoltre, in futuro, un altro programmatore che apportasse modifiche al programma potrebbe dimenticarsi di chiamare tale funzione.

D'altra parte, non possiamo modificare la libreria, in quanto è gestita da un'altra organizzazione, e comunque è già usata da altre applicazioni per le quali il comportamento di default è adeguato.

Un'altra soluzione sarebbe duplicare completamente la classe, e modificare solo la copia. Ma questo porta al raddoppio della dimensione del codice sorgente nonché al raddoppio della dimensione del codice eseguibile. Avere sorgenti ridondanti complica la manutenzione, e avere eseguibili ridondanti li rende meno efficienti. Con l'eccezione di alcuni sistemi embedded, gli eseguibili ridondanti oggi non sono più un grave problema, ma i sorgenti ridondanti invece lo sono sicuramente.

La soluzione offerta dal C++ è costruire una nuova classe che sfrutti tutte le funzionalità della classe esistente, ma modifichi solo la gestione del colore delle scritte. Questo però non è possibile se non opportunamente previsto nel progetto della classe di libreria.

Il nostro problema si può risolvere se la classe "CampoValuta" contiene una funzione membro con la seguente dichiarazione:

virtual Colore colore_testo_default() const;

Si noti la parola-chiave "virtual", posta prima della dichiarazione della funzione membro. Tale parola-chiave indica che il metodo che la segue è ridefinibile, come si vedrà più avanti.

L'implementazione di questa funzione nella libreria si limita a ritornare un oggetto che rappresenta il colore nero, che è quello di default. Internamente, la libreria chiama questa funzione ogni volta che ha bisogno di sapere con quale colore deve essere mostrato il testo nei campi valutari.

Dedicare un'intera funzione a questo semplice compito sembra uno spreco, ma è proprio questa la condizione che ci permette di risolvere il nostro problema. Infatti, esistendo tale funzione, si può scrivere in un file di intestazione della nostra applicazione il seguente codice, in cui si suppone che "BLU" sia una costante definita in precedenza nella nostra applicazione:

struct CampoValutaBlu: CampoValuta {
   Colore colore_testo_default() const {
      return BLU;
   }
};

La prima riga definisce la classe "CampoValutaBlu" come classe derivata (o sottoclasse) della classe "CampoValuta". La classe "CampoValuta" si dice che è la classe base (o la superclasse) della classe "CampoValutaBlu".

Una classe derivata è una classe che contiene tutti i membri della sua superclasse, ma che tipicamente aggiunge nuovi membri o ridefinisce membri esistenti. Tutti i membri che passano automaticamente dalla classe base alla classe derivata si dicono ereditati. Pertanto, tale costrutto, viene chiamato principalmente "ereditarietà", ma sono in uso anche le espressioni "derivazione" e, in inglese, "subclassing".

In questo caso, la classe "CampoValutaBlu" contiene di suo un solo membro, che è la funzione "colore_testo_default". Tale funzione membro ha la stessa firma di una funzione della classe base, pertanto si dice che tale funzione viene ridefinita (in inglese, e spesso anche in italiano, si dice "overriden", che letteramente significa "scavalcata").

A questo punto, basta utilizzare nella nostra applicazione oggetti di tipo "CampoValutaBlu" invece che di tipo "CampoValuta". Ogni volta che si deve creare un widget per la gestione degli importi valutari, si istanzierà la classe "CampoValutaBlu".

A parte la creazione, tali oggetti verranno trattati dal codice applicativo esattamente come se fossero di tipo "CampoValuta". Accedendo a una variabile membro definita in "CampoValuta", si accederà all'istanza della variabile dichiarata in "CampoValuta", e chiamando una funzione membro dichiarata in "CampoValuta" verrà chiamata tale funzione membro, che al suo interno non saprà di far parte di una nuova sottoclasse.

L'eccezione a questa regola si ha proprio quando all'interno della libreria viene chiamata la funzione "colore_testo_default".

Se si fosse omessa la parola-chiave "virtual", il programma sarebbe stato sintatticamente corretto, ma gli oggetti di tipo "CampoValutaBlu" si sarebbero comportati identicamente agli oggetti di tipo "CampoValuta", rendendo inutile la derivazione. Se invece, come in questo caso, la funzione membro è dichiarata con l'attributo "virtual", quando all'interno della libreria serve sapere il colore dei campi valutari, viene chiamata la funzione ridefinita invece di quella originale.

Una classe può definire più funzioni virtuali, per consentire di personalizzare vari aspetti. Per esempio, si potrebbe pensare a funzioni membro virtuali come le seguenti:

virtual Colore colore_sfondo_default() const;
virtual float altezza_default() const;

In tutti questi casi, la libreria fornisce dei valori o comportamenti di default. Se il comportamento di default di una funzione membro è considerato adeguato, non è necessario ridefinire tale funzione.