14.4. Ocultación de nombres

Si se ha heredado de una clase y se proporciona una nueva definición para alguna de sus funciones miembros, existen dos posibilidades. La primera es proporcionar los mismos argumentos y el mismo tipo de retorno en la definición de la clase derivada como la clase base. Esto es conocido como redefinición para funciones miembro ordinarias y sobrecarga, cuando la función miembro de la clase es una función virtual (las funciones virtuales son un caso normal y serán tratadas en detalle en el capítulo 15). Pero ¿qué ocurre cuando se modifican los argumentos de la función miembro o el tipo de retorno en una clase derivada? Aquí esta un ejemplo:

//: C14:NameHiding.cpp
// Hiding overloaded names during inheritance
#include <iostream>
#include <string>
using namespace std;

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

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

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

class Derived3 : public Base {
public:
  // 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
  Derived3 d3;
//!  x = d3.f(); // return int version hidden
  Derived4 d4;
//!  x = d4.f(); // f() version hidden
  x = d4.f(1);
} ///:~

Listado 14.8. C14/NameHiding.cpp


En Base se observa una función sobrecargada f(), en Derived1 no se realiza ningún cambio a f() pero se redefine la función g(). En main(), se observa que ambas funciones f() están disponibles en Derived1. Sin embargo, Derived2 redefine una versión sobrecargada de f() pero no la otra, y el resultado es que la segunda forma de sobrecarga no esta disponible. En Derived3, se ha cambiado el tipo de retorno y esconde ambas versiones de la clase base, y Derived4 muestra que al cambiar la lista de argumentos también se esconde las versiones de la clase base. En general, usted puede expresar cada vez que redefine una función sobrecargada de la clase base, que todas las otras versiones son automáticamente escondidas en la nueva clase. En el capítulo 15, verá como añadir la palabra reservada virtual que afecta un significativamente a la sobrecarga de una función.

Si cambia la interfaz de la clase base modificando la signatura y/o el tipo de retorno de una función miembro desde la clase base, entonces esta utilizando la clase de una forma diferente en que la herencia esta pensado para realizar normalmente. Esto no quiere decir que lo que este haciendo sea incorrecto, esto es que el principal objetivo de la herencia es soportar el polimorfismo, y si usted cambia la signatura de la función o el tipo de retorno entonces realmente esta cambiando la interfaz de la clase base. Si esto es lo que esta intentando hacer entonces esta utilizando la herencia principalmente para la reutilización de código, no para mantener interfaces comunes en la clase base (que es un aspecto esencial del poliformismo). En general, cuando usa la herencia de esta forma significa que esta en una clase de propósito general y la especialización para una necesidad particular - que generalmente, pero no siempre, se considera parte de la composición.

Por ejemplo, considere la clase Stack del capítulo 9. Uno de los problemas con esta clase es que se tenía que realizar que convertir cada vez que se conseguía un puntero desde el contenedor. Esto no es solo tedioso, también inseguro - se puede convertir a cualquier cosa que desee.

Un procedimiento que a primera vista parece mejor es especializar la clase general Stack utilizando la herencia. Aquí esta un ejemplo que utiliza la clase del capítulo 9.

//: C14:InheritStack.cpp
// Specializing the Stack class
#include "../C09/Stack4.h"
#include "../require.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

class StringStack : public Stack {
public:
  void push(string* str) {
    Stack::push(str);
  }
  string* peek() const {
    return (string*)Stack::peek();
  }
  string* pop() {
    return (string*)Stack::pop();
  }
  ~StringStack() {
    string* top = pop();
    while(top) {
      delete top;
      top = pop();
    }
  }
};

int main() {
  ifstream in("InheritStack.cpp");
  assure(in, "InheritStack.cpp");
  string line;
  StringStack textlines;
  while(getline(in, line))
    textlines.push(new string(line));
  string* s;
  while((s = textlines.pop()) != 0) { // No cast!
    cout << *s << endl;
    delete s;
  }
} ///:~

Listado 14.9. C14/InheritStack.cpp


Como todas las funciones miembros en Stack4.h son inline, no es necesario ser enlazadas.

StringStack especializa Stack para que push() acepte solo punteros a String. Antes, Stack acepta punteros a void, y así el usuario no tenía que realizar una comprobación de tipos para asegurarse que el punteros fuesen insertados. Además, peek() and pop() ahora retornan punteros a String en vez de punteros a void, entonces no es necesario realizar la conversión para utilizar el puntero.

orprendido ¡este mecanismo de comprobación de tipos seguro es gratuito, en push(), peek() y pop! Al compilador se le proporciona información extra acerca de los tipos y éste lo utiliza en tiempo de compilación, pero las funciones son inline y no es necesario ningún código extra.

La ocultación de nombres entra en acción en la función push() que tiene la signatura diferente: la lista de argumentos. Si se tuviesen dos versiones de push() en la misma clase, tendrían que ser sobrecargadas, pero en este caso la sobrecarga no es lo que deseamos porque todavía permitiría pasar cualquier tipo de puntero a push como void *. Afortunadamente, C++ esconde la versión push (void *) en la clase base en favor de la nueva versión que es definida en la clase derivada, de este modo, solo se permite utilizar la función push() con punteros a String en StringStack.

Ahora podemos asegurar que se conoce exactamente el tipo de objeto que esta en el contenedor, el destructor funcionará correctamente y problema esta resuelto - o al menos, un parte del procedimiento. Si utiliza push( ) con un puntero a String en StringStack, entonces (según el significado de StringStack) también se esta pasando el puntero a StringStack. Si utiliza pop(), no solo consigue puntero, sino que a la vez el propietario. Cualquier puntero que se haya dejado en StringStack será borrado cuando el destructor sea invocado. Puesto que siempre son punteros a Strings y la declaración delete esta funcionando con punteros a String en vez de punteros a void, la destrucción se realiza de forma adecuada y todo funciona correctamente.

Esto es una desventaja: esta clase solo funciona con punteros de cadenas. Si se desea un Stack que funcione con cualquier variedad de objetos, se debe escribir una nueva versión de la clase que funcione con ese nuevo tipo de objeto. Esto puede convertirse rápidamente en una tarea tediosa, pero finalmente es resulta utilizando plantillas como se vera en el capítulo 16.

Existen consideraciones adicionales sobre este ejemplo: el cambio de la interfaz en Stack en el proceso de herencia. Si la interfaz es diferente, entonces StringStack no es realmente un Stack, y nunca será posible usar de forma correcta un StringStack como Stack. Esto hace que el uso de la herencia sea cuestionable en este punto; si no se esta creando un StringStack del tipo Stack, entonces, porque hereda de él. Más adelante, sen este mismo capítulo se mostrará una versión más adecuada.