15.9. Sobrecargar y redefinir

En el capítulo 14 se vio que redefinir una función sobrecargada en la función base oculta todas las otras versiones de esa función. Cuando se involucra a las funciones virtuales el comportamiento es un poco diferente. Consideremos una versión modificada del ejemplo NameHiding.cpp del capítulo 14:

//: C15:NameHiding2.cpp
// Virtual functions restrict overloading
#include <iostream>
#include <string>
using namespace std;

class Base {
public:
  virtual int f() const { 
    cout << "Base::f()\n"; 
    return 1; 
  }
  virtual void f(string) const {}
  virtual void g() const {}
};

class Derived1 : public Base {
public:
  void g() const {}
};

class Derived2 : public Base {
public:
  // Overriding a virtual function:
  int f() const { 
    cout << "Derived2::f()\n"; 
    return 2;
  }
};

class Derived3 : public Base {
public:
  // Cannot change return type:
  //! void f() const{ cout << "Derived3::f()\n";}
};

class Derived4 : public Base {
public:
  // Change argument list:
  int f(int) const { 
    cout << "Derived4::f()\n"; 
    return 4; 
  }
};

int main() {
  string s("hello");
  Derived1 d1;
  int x = d1.f();
  d1.f(s);
  Derived2 d2;
  x = d2.f();
//!  d2.f(s); // string version hidden
  Derived4 d4;
  x = d4.f(1);
//!  x = d4.f(); // f() version hidden
//!  d4.f(s); // string version hidden
  Base& br = d4; // Upcast
//!  br.f(1); // Derived version unavailable
  br.f(); // Base version available
  br.f(s); // Base version abailable
} ///:~

Listado 15.10. C15/NameHiding2.cpp


La primera cosa a resaltar es que en Derived3, el compilador no permitirá cambiar el tipo de retorno de una función sobreescrita (lo permitiría si f() no fuera virtual). ésta es una restricción importante porque el compilador debe garantizar que se pueda llamar de forma "polimórfica" a la función a través de la clase base, y si la clase base está esperando que f() devuelva un int, entonces la versión de f() de la clase derivada debe mantener ese compromiso o si no algo fallará.

La regla que se enseño en el capítulo 14 todavía funciona: si se sobreescribe una de las funciones miembro sobrecargadas de la clase base, las otras versiones sobrecargadas estarán ocultas en la clase derivada. En el main() el código de Derived4 muestra lo que ocurre incluso si la nueva versión de f() no está actualmente sobreescribiendo una función virtual existente de la interfaz - ambas versiones de f() en la clase base estan ocultas por f(int). Sin embargo, si se hace un upcast de d4 a Base, entonces únicamente las versiones de la clase base estarán disponibles (porque es el compromiso de la clase base) y la versión de la clase derivada no está disponible (debido a que no está especificada en la clase base).

15.9.1. Tipo de retorno variante

La clase Derived3 de arriba viene a sugerir que no se puede modificar el tipo de retorno de una función virtual cuando es sobreescrita. En general es verdad, pero hay un caso especial en el que se puede modificar ligeramente el tipo de retorno. Si se está devolviendo un puntero o una referencia a una clase base, entonces la versión sobreescrita de la función puede devolver un puntero o una referencia a una clase derivada. Por ejemplo:

//: C15:VariantReturn.cpp
// Returning a pointer or reference to a derived
// type during ovverriding
#include <iostream>
#include <string>
using namespace std;

class PetFood {
public:
  virtual string foodType() const = 0;
};

class Pet {
public:
  virtual string type() const = 0;
  virtual PetFood* eats() = 0;
};

class Bird : public Pet {
public:
  string type() const { return "Bird"; }
  class BirdFood : public PetFood {
  public:
    string foodType() const { 
      return "Bird food"; 
    }
  };
  // Upcast to base type:
  PetFood* eats() { return &bf; }
private:
  BirdFood bf;
};

class Cat : public Pet {
public:
  string type() const { return "Cat"; }
  class CatFood : public PetFood {
  public:
    string foodType() const { return "Birds"; }
  };
  // Return exact type instead:
  CatFood* eats() { return &cf; }
private:
  CatFood cf;
};

int main() {
  Bird b; 
  Cat c;
  Pet* p[] = { &b, &c, };
  for(int i = 0; i < sizeof p / sizeof *p; i++)
    cout << p[i]->type() << " eats "
         << p[i]->eats()->foodType() << endl;
  // Can return the exact type:
  Cat::CatFood* cf = c.eats();
  Bird::BirdFood* bf;
  // Cannot return the exact type:
//!  bf = b.eats();
  // Must downcast:
  bf = dynamic_cast<Bird::BirdFood*>(b.eats());
} ///:~

Listado 15.11. C15/VariantReturn.cpp


La función miembro Pet::eats() devuelve un puntero a PetFood. En Bird, ésta función miembro es sobreescrita exactamente como en la clase base, incluyendo el tipo de retorno. Esto es, Bird::eats() hace un >upcast de BirdFood a PetFood en el retorno de la función.

Pero en Cat, el tipo devuelto por eats() es un puntero a CatFood, que es un tipo derivado de PetFood. El hecho de que el tipo de retorno esté heredado del tipo de retorno la función de la clase base es la única razón que hace que esto compile. De esta forma el acuerdo se cumple totalmente: eats() siempre devuelve un puntero a PetFood.

Si se piensa de forma polimórfica lo anterior no parece necesario. ¿Por qué no simplemente se hacen upcast de todos los tipos retornados a PetFood* como lo hace Bird::eats()? Normalmente esa es una buena solución, pero al final del main() se puede ver la diferencia: Cat::eats() puede devolver el tipo exacto de PetFood, mientras que al valor retornado por Bird::eats() hay que hacerle un downcast al tipo exacto.

Devolver el tipo exacto es un poco más general y además no pierde la información específica de tipo debida al upcast automático. Sin embargo, devolver un tipo de la clase base generalmente resuelve el problema por lo que esto es una característica bastante específica.