A menudo en el diseño, se quiere la clase base para presentar
sólo una interfaz para sus clases
derivadas. Esto es, se puede querer que nadie pueda crear un
objeto de la clase base y que ésta sirva únicamente para hacer un
upcast hacia ella, y poder tener
una interfaz. Se consigue haciendo a la clase
abstract (abstracta), poniendo como
mínimo una función virtual pura. Se puede
reconocer a una función virtual pura porque usa la palabra
reservada virtual
y es seguida por
=0
. Si alguien intenta hacer un objeto de una
clase abstracta, el compilador lo impide. Esta es una utilidad que
fuerza a un diseño en concreto.
Cuando se hereda una clase abstracta, hay que implementar todas las funciones virtuales, o la clase que hereda se convierte en una nueva clase abstracta. Crear una función virtual pura permite poner una fución miembro en una interfaz sin forzar a proveer un cuerpo con código sin significado para esa función miembro. Al mismo tiempo, una función virtual fuerza a las clases que la hereden a que implemente una definición para ellas.
En todos los ejemplos de los intrumentos, las funciones en la
clase base Instrument
eran siempre
funciones «tontas». Si esas funciones hubieran sido
llamadas algo iba mal. Esto es porque la intención de la clase
Instrument
es crear una interfaz común
para todas las clases que deriven de ella.
La única razón para establecer una interfaz común es que después
se pueda expresar de forma diferente en cada subtipo. Se crea una
forma básica que tiene lo que está en común con todas las clases
derivadas y nada más. Por esto, Instrument
es
un candidato perfecto para ser una clase abstracta. Se crea una
clase abstracta sólo cuando se quiere manipular un conjunto de
clases a través de una interfaz común, pero la interfaz común no
necesita tener una implementación (o como mucho, no necesita una
implementación completa).
Si se tiene un concepto como Instrument
que
funciona como clase abstracta, los objetos de esa clase casi nunca
tendrán sentido. Es decir, Instrument
sirve
solamente para expresar la interfaz, y no una implementación
particular, por lo que crear un objeto que sea únicamente un
Instrument
no tendrá sentido, y
probablemente se quiera prevenir al usuario de hacerlo. Se puede
solucionar haciendo que todas las funciones virtuales en
Instrument
muestren mensajes de error, pero
retrasa la aparición de los errores al tiempo de ejecución lo que
obligará a un testeo exhaustivo por parte del usuario. Es mucho
más productivo cazar el problema en tiempo de compilación.
Aquí está la sintaxis usada para una función virtual pura:
virtual void f() = 0;
Haciendo esto, se indica al compilador que reserve un hueco para una función en la VTABLE, pero que no ponga una dirección en ese hueco. Incluso aunque sólo una función en una clase sea declarada como virtual pura, la VTABLE estará incompleta.
Si la VTABLE de una clase está incompleta, ¿qué se supone que debe hacer el compilador cuando alguien intente crear un objeto de esa clase? No sería seguro crear un objeto de esa clase abstracta, por lo que se obtendría un error de parte del compilador. Dicho de otra forma, el compilador garantiza la pureza de una clase abstracta. Hacer clases abstractas asegura que el programador cliente no puede hacer mal uso de ellas.
Aquí tenemos Instrument4.cpp
modificado para
usar funciones virtuales puras. Debido a que la clase no tiene
otra cosa que no sea funciones virtuales, se la llama
clase abstracta pura:
//: C15:Instrument5.cpp // Pure abstract base classes #include <iostream> using namespace std; enum note { middleC, Csharp, Cflat }; // Etc. class Instrument { public: // Pure virtual functions: virtual void play(note) const = 0; virtual char* what() const = 0; // Assume this will modify the object: virtual void adjust(int) = 0; }; // Rest of the file is the same ... class Wind : public Instrument { public: void play(note) const { cout << "Wind::play" << endl; } char* what() const { return "Wind"; } void adjust(int) {} }; class Percussion : public Instrument { public: void play(note) const { cout << "Percussion::play" << endl; } char* what() const { return "Percussion"; } void adjust(int) {} }; class Stringed : public Instrument { public: void play(note) const { cout << "Stringed::play" << endl; } char* what() const { return "Stringed"; } void adjust(int) {} }; class Brass : public Wind { public: void play(note) const { cout << "Brass::play" << endl; } char* what() const { return "Brass"; } }; class Woodwind : public Wind { public: void play(note) const { cout << "Woodwind::play" << endl; } char* what() const { return "Woodwind"; } }; // Identical function from before: void tune(Instrument& i) { // ... i.play(middleC); } // New function: void f(Instrument& i) { i.adjust(1); } int main() { Wind flute; Percussion drum; Stringed violin; Brass flugelhorn; Woodwind recorder; tune(flute); tune(drum); tune(violin); tune(flugelhorn); tune(recorder); f(flugelhorn); } ///:~
Listado 15.6. C15/Instrument5.cpp
Las funciones virtuales puras son útiles porque hacen explícita la abstracción de una clase e indican al usuario y al compilador cómo deben ser usadas.
Hay que hacer notar que las funciones virtuales puras previenen a una clase abstracta de ser pasadas a una función por valor, lo que es una manera de prevenir el object slicing (que será descrito de forma reducida). Convertir una clase en abstracta también permite garantizar que se use siempre un puntero o una referencia cuando se haga upcasting a esa clase.
Sólo porque una función virtual pura impida a la VTABLE estar completa no implica que no se quiera crear cuerpos de función para alguna de las otras funciones. A menudo se querrá llamar a la versión de la función que esté en la clase base, incluso aunque ésta sea virtual. Es una buena idea poner siempre el código común tan cerca como sea posible de la raiz de la jerarquía. No sólo ahorra código, si no que permite fácilmente la propagación de cambios.
Es posible proveer una definición para una función virtual pura en la clase base. Todavía implica decirle al compilador que no permita crear objetos de esa clase base abstracta, y que las funciones virtuales puras deben ser definidas en las clases derivadas para poder crear objetos. Sin embargo, puede haber un trozo de código en común que se quiera llamar desde todas, o algunas de las clases derivadas en vez de estar duplicando código en todas las funciones.
Este es un ejemplo de definición de funciones virtuales.
//: C15:PureVirtualDefinitions.cpp // Pure virtual base definitions #include <iostream> using namespace std; class Pet { public: virtual void speak() const = 0; virtual void eat() const = 0; // Inline pure virtual definitions illegal: //! virtual void sleep() const = 0 {} }; // OK, not defined inline void Pet::eat() const { cout << "Pet::eat()" << endl; } void Pet::speak() const { cout << "Pet::speak()" << endl; } class Dog : public Pet { public: // Use the common Pet code: void speak() const { Pet::speak(); } void eat() const { Pet::eat(); } }; int main() { Dog simba; // Richard's dog simba.speak(); simba.eat(); } ///:~
Listado 15.7. C15/PureVirtualDefinitions.cpp
El hueco en la VTABLE de Pet
todavía
está vacío, pero tiene funciones a las que se puede llamar desde
la clase derivada.
Otra ventaja de esta característica es que perimite cambiar de una función virtual corriente a una virtual pura sin destrozar el código existente (es una forma para localizar clases que no sobreescriban a esa función virtual).