Un uso no controvertido de la herencia múltiple es la herencia de interfaz. En C++, toda herencia lo es de implementación, dado que todo en una clase base, interface e implentación, pasa a formar parte de la clase derivada. No es posible heredar solo una parte de una clase (es decir, la interface únicamente). Tal como se explica en el [FIXME:enlace en la versión web] Capítulo 14 del volumen 1, es posible hacer herencia privada y protegida para restringir el acceso a los miembros heredados desde las clases base cuando se usa por clientes de instancias de una clase derivada, pero esto no afecta a la propia clase derivada; esa clase sigue conteniendo todos los datos de la clase base y puede acceder a todos los miembros no-privados de la clase base.
La herencia de interfaces. por otra parte, sólo añade declaraciones de miembros a la interfaz de la clase derivada, algo que no está soportado directamente en C++. La técnica habitual para simular la herencia de interfaz en C++ es derivar de una clase interfaz, que es una clase que sólo contiene declaraciones (ni datos ni cuerpos de funciones). Estas declaraciones serán funciones virtuales puras, excepto el destructor. Aquí hay un ejemplo:
//: C09:Interfaces.cpp // Multiple interface inheritance. #include <iostream> #include <sstream> #include <string> using namespace std; class Printable { public: virtual ~Printable() {} virtual void print(ostream&) const = 0; }; class Intable { public: virtual ~Intable() {} virtual int toInt() const = 0; }; class Stringable { public: virtual ~Stringable() {} virtual string toString() const = 0; }; class Able : public Printable, public Intable, public Stringable { int myData; public: Able(int x) { myData = x; } void print(ostream& os) const { os << myData; } int toInt() const { return myData; } string toString() const { ostringstream os; os << myData; return os.str(); } }; void testPrintable(const Printable& p) { p.print(cout); cout << endl; } void testIntable(const Intable& n) { cout << n.toInt() + 1 << endl; } void testStringable(const Stringable& s) { cout << s.toString() + "th" << endl; } int main() { Able a(7); testPrintable(a); testIntable(a); testStringable(a); } ///:~
Listado 8.1. C09/Interfaces.cpp
La clase Able
«implementa»
las interfaces Printable
,
Intable
y Stringable
dado que proporciona
implementaciones para las funciones que éstas declaran. Dado
que Able
deriva de las tres clases, los
objetos Able
tienen múltiples
relaciones «es-un». Por ejemplo, el
objeto a
puede actuar como un
objeto Printable
dado que su
clase, Able
, deriva públicamente
de Printable
y proporciona una
implementación para print()
. Las
funciones de prueba no necesitan saber el tipo más derivado de
su parámetro; sólo necesitan un objeto que sea substituible por
el tipo de su parámetro.
Como es habitual, una plantilla es una solución más compacta:
//: C09:Interfaces2.cpp // Implicit interface inheritance via templates. #include <iostream> #include <sstream> #include <string> using namespace std; class Able { int myData; public: Able(int x) { myData = x; } void print(ostream& os) const { os << myData; } int toInt() const { return myData; } string toString() const { ostringstream os; os << myData; return os.str(); } }; template<class Printable> void testPrintable(const Printable& p) { p.print(cout); cout << endl; } template<class Intable> void testIntable(const Intable& n) { cout << n.toInt() + 1 << endl; } template<class Stringable> void testStringable(const Stringable& s) { cout << s.toString() + "th" << endl; } int main() { Able a(7); testPrintable(a); testIntable(a); testStringable(a); } ///:~
Listado 8.2. C09/Interfaces2.cpp
Los
nombres Printable
, Intable
y Stringable
ahora no son mas que
parámetros de la plantilla que asume la existencia de las
operaciones indicadas en sus respectivos argumentos. En otras
palabras, las funciones de prueba pueden aceptar argumentos de
cualquier tipo que proporciona una definición de método con la
signatura y tipo de retorno correctos. Hay gente que encuentra
más cómoda la primera versión porque los nombres de tipo
garantizan que las interfaces esperadas están
implementadas. Otros están contentos con el hecho de que si las
operaciones requeridas por las funciones de prueba no se
satisfacen por los argumentos de la plantilla, el error puede
ser capturado en la compilación. Esta segunda es una forma de
comprobación de tipos técnicamente más débil que el primer
enfoque (herencia), pero el efecto para el programador (y el
programa) es el mismo. Se trata de una forma de comprobación
débil de tipo que es aceptable para muchos de los programadores
C++ de hoy en día.