15.4. Funciones virtuales

Para que la ligadura dinámica tenga efecto en una función particular, C++ necesita que se use la palabra reservada virtual cuando se declara la función en la clase base. La ligadura en tiempo de ejecución funciona unícamente con las funciones virtual es, y sólo cuando se está usando una dirección de la clase base donde exista la función virtual, aunque puede ser definida también en una clase base anterior.

Para crear una función miembro como virtual, simplemente hay que preceder a la declaración de la función con la palabra reservada virtual. Sólo la declaración necesita la palabra reservada virtual, y no la definición. Si una función es declarada como virtual, en la clase base, será entonces virtual en todas las clases derivadas. La redefinición de una función virtual en una clase derivada se conoce como overriding.

Hay que hacer notar que sólo es necesario declarar la función como virtual en la clase base. Todas las funciones de las clases derivadas que encajen con la declaración que esté en la clase base serán llamadas usando el mecanismo virtual. Se puede usar la palabra reservada virtual en las declaraciones de las clases derivadas (no hace ningún mal), pero es redundante y puede causar confusión.

Para conseguir el comportamiento deseado de Instrument2.cpp, simplemente hay que añadir la palabra reservada virtual en la clase base antes de play().

//: C15:Instrument3.cpp
// Late binding with the virtual keyword
#include <iostream>
using namespace std;
enum note { middleC, Csharp, Cflat }; // Etc.

class Instrument {
public:
  virtual void play(note) const {
    cout << "Instrument::play" << endl;
  }
};

// Wind objects are Instruments
// because they have the same interface:
class Wind : public Instrument {
public:
  // Override interface function:
  void play(note) const {
    cout << "Wind::play" << endl;
  }
};

void tune(Instrument& i) {
  // ...
  i.play(middleC);
}

int main() {
  Wind flute;
  tune(flute); // Upcasting
} ///:~

Listado 15.2. C15/Instrument3.cpp


Este archivo es idéntico a Instrument2.cpp excepto por la adición de la palabra reservada virtual y, sin embargo, el comportamiento es significativamente diferente: Ahora la salida es Wind::play.

15.4.1. Extensibilidad

Con play() definido como virtual en la clase base, se pueden añadir tantos nuevos tipos como se quiera sin cambiar la función play(). En un programa orientado a objetos bien diseñado, la mayoría de las funciones seguirán el modelo de play() y se comunicarán únicamente a través de la interfaz de la clase base. Las funciones que usen la interfaz de la clase base no necesitarán ser cambiadas para soportar a las nuevas clases.

Aquí está el ejemplo de los instrumentos con más funciones virtuales y un mayor número de nuevas clases, las cuales trabajan de manera correcta con la antigua (sin modificaciones) función play():

//: C15:Instrument4.cpp
// Extensibility in OOP
#include <iostream>
using namespace std;
enum note { middleC, Csharp, Cflat }; // Etc.

class Instrument {
public:
  virtual void play(note) const {
    cout << "Instrument::play" << endl;
  }
  virtual char* what() const {
    return "Instrument";
  }
  // Assume this will modify the object:
  virtual void adjust(int) {}
};

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); }

// Upcasting during array initialization:
Instrument* A[] = {
  new Wind,
  new Percussion,
  new Stringed,
  new Brass,
};

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.3. C15/Instrument4.cpp


Se puede ver que se ha añadido otro nivel de herencia debajo de Wind, pero el mecanismo virtual funciona correctamente sin importar cuantos niveles haya. La función adjust() no está redefinida (override) por Brass y Woodwind. Cuando esto ocurre, se usa la definición más "cercana" en la jerarquía de herencia - el compilador garantiza que exista alguna definición para una función virtual, por lo que nunca acabará en una llamada que no esté enlazada con el cuerpo de una función (lo cual sería desatroso).

El array A[] contiene punteros a la clase base Instrument, lo que implica que durante el proceso de inicialización del array habrá upcasting. Este array y la función f() serán usados en posteriores discusiones.

En la llamada a tune(), el upcasting se realiza en cada tipo de objeto, haciendo que se obtenga siempre el comportamiento deseado. Se puede describir como "enviar un mensaje a un objeto y dejar al objeto que se preocupe sobre qué hacer con él". La función virtual es la lente a usar cuando se está analizando un proyecto: ¿Dónde deben estar las clases base y cómo se desea extender el programa? Sin embargo, incluso si no se descubre la interfaz apropiada para la clase base y las funciones virtuales durante la creación del programa, a menudo se descubrirán más tarde, incluso mucho más tarde cuando se desee ampliar o se vaya a hacer funciones de mantenimiento en el programa. Esto no implica un error de análisis o de diseño; simplemente significa que no se conocía o no se podía conocer toda la información al principio. Debido a la fuerte modularización de C++, no es mucho problema que esto suceda porque los cambios que se hagan en una parte del sistema no tienden a propagarse a otras partes como sucede en C.