15.9. Sobrecargar y redefinir

En el Capítulo 14 se vio que redefinir una función sobrecargada en la clase base oculta todas las demás versiones de esa función de la clase base. Cuando hay funciones virtuales de por medio, el comportamiento es un poco diferente. Considere 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


Lo primero que hay que resaltar es que en Derived3, el compilador no permitirá cambiar el tipo de retorno de una función redefinida (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 contrato o si no algo fallará.

La regla que se enseñó en el Capítulo 14 todavía funciona: si se redefine 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 que prueba Derived4 muestra que esto ocurre incluso si la nueva versión de f() no está realmente redefiniendo la interfaz de una función virtual existente - ambas versiones de f() de la clase base están 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 lo que promete el contrato 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 redefinida. 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 redefinida de la función puede devolver un puntero o una referencia a una clase derivada de la que devuelve la base. 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, esta función miembro es redefinida 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 de la función de la clase base es la única razón que hace que esto compile. De esta forma el contrato se sigue cumpliendo: eats() siempre devuelve un puntero a PetFood.

Si se piensa de forma polimórfica lo anterior no parece necesario. ¿Por qué no hacer simplemente upcast de todos los tipos de retorno a PetFood*, como hace Bird::eats()? Normalmente ésa 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.

Así pues, poder devolver el tipo exacto es un poco más general y no se pierde la información específica de tipo por el upcast automático. Sin embargo, devolver el tipo base generalmente resuelve los problemas, por lo que ésta es una característica bastante especializada.