Nella lezione C++ sull'overloading degli operatori, abbiamo accennato al caso particolare degli operatori di inserimento ed estrazione da stream. In quel contesto abbiamo visto che, pur essendo in generale disponibili varie modalità di sovraccarico, per via dell'interazione con le classi definite nelle librerie standard, tali operatori dovrebbero essere implementati come funzioni esterne per tutti i tipi definiti dall'utente.
In questo modo infatti, in conformità alla convenzione adottata nelle librerie standard, non è necessario invertire l'ordine degli operandi al momento dell'invocazione.
Tuttavia, a volte può essere richiesto di ridefinire tali operatori nel contesto di una gerarchia di classi, ed in tale ottica l'uso di funzioni esterne e non di metodi potrebbe porre qualche ostacolo alla gestione del nostro modelli dati in senso polimorfico.
Consideriamo ad esempio, il listato seguente, in cui una sono definite due classi, Base e Derived, legate da un vincolo gerarchico. Sia la classe base che la sua derivata, ridefiniscono l'operatore di inserimento <<
come funzione esterna e friend.
#include <iostream>
class Base
{
public:
Base() {}
virtual ~Base() {}
friend std::ostream& operator<<(std::ostream &out, const Base &b)
{
out << "Base";
return out;
}
};
class Derived : public Base
{
public:
Derived() {}
friend std::ostream& operator<<(std::ostream &out, const Derived &d)
{
out << "Derived";
return out;
}
};
int main()
{
Base b;
std::cout << b << '\n'; // stampa "Base"
Derived d;
std::cout << d <<'\n'; // stampa "Derived"
Base* dptr = &d;
std::cout << *dptr << '\n'; // stampa "Base" ?!?
return 0;
}
Come illustrato nel corpo della funzione main()
, questa particolare implementazione funziona perfettamente se non si sconfina nell'ambito del comportamento polimorfico, cioè quando si fa uso di puntatori alla classe base per dereferenziare un'istanza di una classe derivata. Quando infatti usiamo il puntatore dptr per dereferenziare l'oggetto d, otteniamo un comportamento, probabilmente inatteso, che consiste nell'invocazione del sovraccarico della classe base piuttosto che quello della derivata.
In realtà ciò non deve stupirci perchè ad una funzione esterna non può essere applicato il qualificatore virtual, e da ciò ne deriva il fatto che l'invocazione del sovraccarico più adatto venga risolta a tempo di compilazione invece che a tempo di esecuzione. In questo caso quindi, la scelta sarebbe dettata dal tipo nominale dell'argomento dptr cioè Base*, e non dal tipo specifico dell'istanza da esso puntata, cioè Derived.
Il problema quindi consiste nel preservare il comportamento polimorfico, o meglio dinamico, degli oggetti della gerarchia pur non potendo sovraccaricare tali operatori come membri virtuali di classe.
Fortunatamente, la soluzione è molto semplice e consiste nel disaccoppiare la definizione dell'operatore in quanto funzione esterna dall'implementazione effettiva che viene delegata ad un metodo virtuale della classe, come mostrato nel listato seguente:
#include <iostream>
class Base
{
public:
Base() {}
virtual ~Base() {}
protected:
virtual void print(std::ostream &out) const
{
out << "Base";
}
friend std::ostream& operator<<(std::ostream &out, const Base &b)
{
b.print(out); // delega della stampa ad un metodo della classe
return out;
}
};
class Derived : public Base
{
public:
Derived() {}
protected:
virtual void print(std::ostream &out) const override
{
out << "Derived";
}
};
int main()
{
Base b;
std::cout << b << '\n'; // stampa "Base"
Derived d;
std::cout << d <<'\n'; // stampa "Derived"
Base* dptr = &d;
std::cout << *dptr << '\n'; // stampa "Derived"
return 0;
}
Il metodo print()
è virtuale, e ciò consente di specializzarne il comportamento nelle classi derivate mediante sovraccarico. Inoltre esso è un metodo const poichè l'argomento b dell'operatore di inserimento è decorato con il qualificatore const
che lo rende un valore non modificabile, ed in quanto tale può invocare solo metodi che garantiscono di non apportare modifiche all'istanza che li ha invocati.
Inoltre si osservi che la definizione del sovraccarico dell'operatore <<
come funzione esterna appare solamente nella classe base. La classi derivate devono fornire il solo sovraccarico del medoto virtuale print()
laddove ciò sia necessario. Considerazioni del tutto analoghe valgono per la ridefinizione dell'operatore di estrazione dal flusso >>
in un contesto gerarchico.
Si consideri inoltre che il comportamento polimorfico è garantito anche rispetto le classi stream della libreria standard. Ad esempio, quest'unica ridefinizione dell'operatore <<
è valida anche per le istanze di ofstream e fstream per la scrittura su file.
Sostituendo il corpo della funzione main()
del listato precedente con quello che appare nel frammento seguente è infatti possibile riprodurre l'output generato sulla riga di comando anche sul file out.txt, che verrà generato nella stessa locazione del file system in cui si trova il nostro eseguibile.
#include <fstream>
// definizione di Base e Derived come sopra
int main()
{
std::ofstream ofout("out.txt");
if (ofout.is_open())
{
ofout << b << '\n';
ofout << d << '\n';
ofout << *dptr << '\n';
ofout.close();
}
return 0;
}