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).
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.