cerrar-sesion editar-perfil marker video calendario monitor periodico fax rss twitter facebook google-plus linkedin alarma circulo-derecha abajo derecha izquierda mover-vertical candado usuario email lupa exito mapa email2 telefono etiqueta

400510404. Simulación de Operadores Polimorfos en C++

Escrito por Redacción en Tema de portada
no hay comentarios Haz tu comentario
Imagen de logotipo de facebook Imagen de logotipo de Twitter Imagen de Logotipo de Google+ Imagen de logotipo de Linkedin

En C++, el polimorfismo permite que una variable se refiera a objetos de distintos tipos de datos. El único problema es que los distintos tipos de datos tienen que ser miembros de la misma jerarquía de herencia y tienen que estar por debajo en la jerarquía del tipo de datos de la variable.

Lo bueno es que esta capacidad permite que la misma instrucción se comporte de forma diferente, basándose en el tipo de datos del objeto en cuestión y no en el tipo de datos de la variable.

Consideremos la jerarquía del diagrama de clase UML de la Figura 1. Aquí, he derivado las clases Manager y Salesperson de la clase Employee. Ello me permite usar un puntero Employee para señalar a un objeto Employee, Manager, Salesperson.

El código que sigue es un intento de polimorfismo:

Employee* pEmp = new Manager;

cout << *pEmp << endl; Sin embargo, este intento fracasa porque el código no invoca el operador de inserción (<<) de Manager como se pretendía, invoca el operador de inserción de Employee. Ello ocurre porque la operación de dirección hacia dentro (*) se realiza primero, y así *pEmp retorna el tipo de datos al puntero. En este caso, como pEmp es un puntero Employee, la operación *pEmp retorna el tipo de datos Employee, a pesar de indicar hacia un objeto Manager. Después de hacer *pEmp, se empareja la invocación de función mediante el tipo de datos Employee. Por consiguiente, se empareja la función de inserción de Employe y no la función de Manager como se pretendía. En C++, el polimorfismo normalmente necesita de métodos virtuales. Como sólo se pueden heredar métodos, muchos programadores piensan que los operadores de inserción (<<) y de extracción (>>) no pueden mostrar comportamiento polimorfo porque estos operadores están implementados como funciones amigas no miembros.

Sin embargo, hay varias formas de hacer polimorfos a estos operadores. En el presente artículo, voy a comparar y a contrastar tres técnicas diferentes.

Versión 1: métodos auxiliares

La primera y más sencilla versión consigue el comportamiento polimorfo añadiendo métodos virtuales extra a cada clase. Mientras que los operadores son funciones no miembro, estos nuevos elementos son métodos y pueden ser polimorfos.

Así, el operador de inserción invoca un método virtual polimorfo de entre la clase y dejamos que el compilador haga el trabajo pesado. Los compiladores normalmente compilan una tabla de punteros de función denominados “virtual function table” (vtable) para ayudar a resolver la invocación a un método virtual.

Cada clase con un método virtual tiene una memoria asignada para una vtable. Además, cada objeto o instancia de una clase con métodos virtuales tiene un puntero a la vtable. Por tanto, el espacio requerido para esta versión es la memoria para la vtable de la clase, además de la memoria para un puntero en todos y cada uno de los objetos. El Listado número 1 presenta esta versión del ejemplo Employee

Versión 2: El operador dynamic_cast

La siguiente versión elimina los métodos virtuales extra de la versión 1 para reducir los requisitos de memoria del programa mientras que mantiene iguales los niveles de funcionalidad. Tengamos en cuenta que la versión 1 necesitaba un puntero extra para todos y cada uno de los objetos.

En la versión 2, simulo el polimorfismo deseado con la funcionalidad C++ Runtime_type_information (RTTI) combinada con el operador dynamic_cast en la función de inserción del padre. El operador dynamic_cast convierte un puntero o referencia de un tipo de objeto a otro tipo de objeto cuando ambos tipos están dentro de la misma jerarquía de clase (véase C++ for Game Programmers,de Noel Llopis; Charles River Media, 2003).

Esta es la única clase de operación de asignación en C++ que se comprueba en el momento de ejecución. Las otras conversiones de tipos o bien no comprueban o comprueban en el momento de la compilación. Si la conversión en el tiempo de ejecución entre los tipos de puntero no es válida, la operación dynamic_cast retorna un puntero nulo (véase The C++ Standard Library: A Tutorial and Reference, de Nicolia M. Josuttis; Addison-Wesley, 1999).

((

Listado 1

class Employee

public:

friend ostream& operator<<(ostream& os, const Employee & aEmployee); virtual ostream& print(ostream& os) const; ;

class Manager : public Employee

public:

friend ostream& operator<<(ostream& os, const Manager & aManager); virtual ostream& print(ostream& os) const; ;

ostream& Employee::print(ostream& os) const

// code to print the Employee class

return os;

ostream& operator<<(ostream& os, const Employee & aEmployee)

return aEmployee.print(os);

ostream& Manager::print(ostream& os) const

Employee::print(os);

// code to print the Manager class

return os;

ostream& operator<<(ostream& os, const Manager & aManager)

return aManager.print(os);

))

La estrategia que utilizo para la versión 2 es usar el operador dynamic_cast en una función padre para averiguar si un objeto que se ha pasado a esta función padre no es en realidad un objeto hijo. En otras palabras, si me han pasado un objeto hijo, proceso la porción del padre del objeto y a continuación invoco la función del hijo para que procese la porción del hijo.

El orden de procesado imita la primera versión polimorfa del código. El Listado número 2 es el ejemplo del Employee para versión 2.

Volviendo a mi intento inicial de usar el operador de inserción (cout << *pEmp), la clase Employee es el padre mientras que la Manager se considera como un hijo. En la función de inserción (<<) del Employee, siempre imprimo las variables privadas de Employee. Después, uso el operador dynamic_cast para ver si me han pasado un objeto Manager. Si ha sido así, entonces invoco la función de inserción de Manager para que se encargue de imprimir las variables privadas de Manager. Así, he impreso la porción de Manager mediante la función de Manager, a pesar de que se invoca la función de Employee. Así es como se consigue el algoritmo simulado. Sin embargo, he visto que al usar el operador dynamic_cast de esta manera se llega a un problema. El error aparece en un uso no polimorfo de las funciones de inserción. Por ejemplo: Manager m1; cout << manager1; A diferencia del primer ejemplo que usa la función <<, esta vez, la función de inserción de Manager se invoca correctamente y se le pasa un objeto Manager. Cuando se está dentro de la función Manager, tengo que enfrentarme a la porción Employee del objeto. Esto se hace normalmente invocando la función del Employee desde dentro de la función del Manager: ostream& operator<<(ostream& os, const Manager& aManager)

os << static_cast(aManager);

// code to print the Manager class

return os;

Por desgracia, esta invocación a la función del Manager nos lleva a un infinito bucle recursivo entre las funciones hijo y padre, porque se están invocando ahora la una a la otra. Para resolver el problema, he introducido una bandera booleana.

Dicha bandera es una variable de función estática, porque su valor ha de ser retenido durante las invocaciones de función anidadas. El Listado número 3 es la versión actualizada de la función de inserción del Manager. Usa la bandera booleana de la que acabo de hablar para detener el problema del bucle recursivo.

[(

Listado 2

ostream& operator<<(ostream& os, const Employee& a Employee)

// code to print the Employee class

if (dynamic_cast(&a Employee) != NULL)

os << *(dynamic_cast(&a Employee));

return os;

)]

Ahora, al invocar la función del Manager directamente mediante el objeto manager1 del segundo ejemplo invoca correctamente la función de inserción del Employee para que imprima la porción Employee de manager1. Tengamos en cuenta que la función de inserción del Employee a continuación invoca la función del Manager pero mi nueva bandera evita que el bucle continúe más lejos. La bandera además evita que la porción de Manager del objeto se imprima dos veces.

((La función de inserción de Manager gestiona con éxito de sus dos usos posibles ser invocado directamente por un objeto Manager, o ser invocado indirectamente por la función de inserción de Employee))

Mientras que esta versión soluciona correctamente la cuestión de ser invocada directamente por un objeto Manager, el ejemplo original que usa un puntero Employee que indica a un objeto Manager tiene un problema, se invoca dos veces la función del Employee.

Se invoca primero la función de Employee porque está procesando un puntero Employee. Como a la función de Employeese le entregó un objeto Manager, invoca correctamente la función de inserción de Manager.

El problema surge dentro de la función de Manager. La función de Manager no debería invocar la función de Employee en este caso. No obstante, no hay forma de decir si fue invocada por la función Employee. Para arreglar el problema, se añade también una bandera booleana a la función Employee; véase el Listado número 4.

[(

Listado 3

ostream& operator<<(ostream& os, const Manager& aManager)

static bool Manager_printed = false;

if (Manager_printed == false)

Manager_printed = true;

os << static_cast(aManager);

Manager_printed = false;

// code to print the Manager class

return os;

)]

Así, la función de inserción de Manager gestiona con éxito de sus dos usos posibles ser invocado directamente por un objeto Manager, o ser invocado indirectamente por la función de inserción de Employee.

En el primer caso, invoca la función de inserción de Employee para que procese la porción de Employee del objeto. En el segundo caso, la porción de Employee se está procesando en otra parte.

[(

Listado 4

ostream& operator<<(ostream& os, const Employee& a Employee)

static bool Employee_printed = false;

if (Employee_printed == false)

// code to print the Employee class

Employee_printed = true;

if (dynamic_cast(&a Employee) != NULL)

os << *(dynamic_cast(&a Employee));

Employee_printed = false;

return os;

)]

Por desgracia me encontré con que el tamaño ejecutable del programa crece cuando se conecta RTTI , así que mi deseo de reducir los requisitos de memoria puede no funcionar tan bien como yo había esperado. Para empeorar las cosas, para que funcione la operación dynamic_cast , la clase padre tiene que tener al menos un método virtual.

Como se necesita un método virtual, el espacio para esta versión es el mismo que para la versión anterior una vtable para la clase, más un puntero a la vtable para cada objeto. Sin embargo, si la clase ya tiene un método virtual por algún otro motivo, entonces no se necesita espacio adicional para que esta versión implemente el polimorfismo.

Versión 3: Un tipo enumerado

La versión final reemplaza el operador dynamic_cast por un identificador de tipos añadido a la clase padre. No se necesita ningún método virtual para ninguna de las clases. Ello me permite reducir la memoria que se necesita para cada objeto, desde un puntero de 32 bits que señala hacia la vtable a un carácter de 8 bits que almacena el identificador de tipos.

Además, no se necesita memoria para la vtable de la clase, puesto que no se usa ni se necesita un método virtual para el operador dynamic_cast porque tampoco se está utilizando este operador. El identificador de tipo es un tipo enumerado almacenado en una variable de caracteres que se añade a la clase Employee; véase el Listado número 5.

((

Listado 5

enum type EMPLOYEE=1, MANAGER=2, SALESPERSON=3 ;

class Employee

public:

char _type;

Employee() _type= EMPLOYEE;

friend ostream& operator<<(ostream& os, const Employee& aEmployee); ;

class Manager : public Employee

public:

Manager() _type = MANAGER;

friend ostream& operator<<(ostream& os, const Manager& aManager); ;

))

Los objetos tienen que inicializar el identificador de tipos en sus constructores. En aras de la brevedad, hice que el identificador de tipos fuera público, pero se podría hacer también privado. Se suministrarían también para esta opción métodos protegidos de accesor y mutador.

El nuevo identificador de tipos sirve dos fines, identifica el tipo de objeto que se ha pasado a la función de inserción del padre, y sustituye la bandera booleana utilizada en la versión anterior. La bandera detiene la recursión infinita entre las dos funciones de inserción. El identificador de tipos puede actuar como bandera si se altera mientras se ejecutan las dos funciones, pero hay que reajustar antes de salir de la operación de inserción.

El Listado número 6 es la versión número 3 del ejemplo. El código para la función de inserción Employee extiende los ejemplos anteriores de manera que funciona en una situación en la que la clase Employee tiene dos hijos, una clase Manager y una Salesperson.

La función de inserción Employee procesa la porción de Employee de la clase antes de comprobar el identificador de tipos. Si se pasó un objeto Manager dentro de la función Employee, se cambia el identificador de tipos, y entonces se invoca la función de Manager.

[(

Listado 6

ostream& operator<<(ostream& os, const Employee& aEmployee)

// code to print the Employee class

if (aEmployee._type == MANAGER)

const_cast(aEmployee)._type = – aEmployee._type;

os << static_cast(aEmployee);

const_cast(aEmployee)._type = – aEmployee._type;

else if (aEmployee._type == SALESPERSON)

const_cast(aEmployee)._type = – aEmployee._type;

os << static_cast(aEmployee);

const_cast(aEmployee)._type = – aEmployee._type;

return os;

ostream& operator<<(ostream& os, const Manager& aManager)

if (aManager._type == MANAGER)

const_cast(aManager)._type = – aManager._type;

os << static_cast(aManager);

const_cast(aManager)._type = – aManager._type;

// code to print the Manager class

return os;

)]

La función de inserción de Manager comprueba el identificador de tipos del objeto antes de invocar la función de inserción Employee. Si se ha cambiado el identificador tipo, entonces sabemos que la función de Manager fue invocada por su padre y así, no debería ser invocada de nuevo.

Si el identificador de tipos no ha sido modificado, entonces sabemos que es el caso no polimorfo y por tanto, tendremos que invocar la función de inserción de Employee para que procese la porción de Employee del objeto.

((

Prueba número 1.

Employee employee1;

cout << employee1; Prueba número 2. Manager m1; cout << manager1; Prueba número 3. Employee* pEmp = new Employee; cout << *pEmp; Prueba número 4. Manager* pManager1 = new Manager; cout << *pManager1; Prueba número 5 Employee* pEmp = new Manager; cout << *pEmp; Prueba número 6. Employee* pEmp = new Salesperson; cout << *pEmp; ))

Aunque el comportamiento parece ser equivalente al de la versión 2, está en realidad ligeramente mejorado. La versión 2 invoca la función del Employee dos veces para el primer ejemplo (cout << *pEmp). Sin embargo, la versión 3 no invoca la función Employee una segunda vez porque la función del Manager ve que la función de Employee cambió el identificador de tipos. Al reemplazar la variable de función estática de la versión 2 con una variable de clase nos permite esta mejora.

¿Cómo se comparan las tres?

El motivo primordial para explorar versiones alternativas de funciones de inserción polimorfas era el de crear una versión que utilizase menos memoria que la versión de método virtual al tiempo que se obtenían unas funcionalidades comparables.

Para comparar las tres versiones de código, experimenté con tres casos de prueba; véase la Tabla 1. Los dos primeros casos de prueba usan objetos y no punteros, así que no son polimorfos.

La primera prueba invoca la función de inserción Employee mediante un objeto Employee. De igual manera, la segunda prueba invoca la función de Manager mediante un objeto Manager . Los dos casos siguientes de pruebas usan punteros en los que el tipo del puntero coincide con el tipo de objeto que está siendo indicado por el puntero.

El tercero utiliza un puntero Employee que está indicando hacia un objeto Employee. El cuarto utiliza un puntero Manager que está indicando hacia un objeto Manager. Los dos últimos casos de pruebas valoran el comportamiento polimorfo de las funciones.

Los dos utilizan un puntero Employee que está indicando hacia un objeto Employee y a un Salesperson respectivamente.

((La primera prueba invoca la función de inserción Employee mediante un objeto Employee. De igual manera, la segunda prueba invoca la función de Manager mediante un objeto Manager))

La Figura 2 muestra los datos sobre el tiempo de la prueba para cada versión. La unidad de tiempo es de millonésimas de segundos. Estos números son la media de varias ejecuciones usando Visual C++ versión 7.

Esperaba que la versión 2 fuera más lenta, porque usa información de tipos en tiempo de ejecución (RTTI, por sus siglas en ingles), pero me sorprendió que lo fuera tantísimo más. Hay que recordar que la versión 2 usaba la misma memoria que la versión 1.

Como esperaba, las versiones 1 y 3 eran comparables en términos de funcionalidad, De hecho, la versión 3 es un poco más rápida en el primer y tercer caso de prueba. Estos dos casos de prueba se encargan de los objetos padre. Sin embargo, la versión 3 es un poco más lenta para los casos de prueba que se encargan de los objetos hijo.

La versión 1 requiere que una clase tenga una vtable, y que cada objeto contenga un puntero a la vtable. La versión 3 requiere que cada objeto tenga un identificador de tipos, pero esto necesita menos memoria que el puntero para una vtable. Así pues, la versión 3 utiliza menos memoria por objeto y por clase.

Conclusión

En este artículo he presentado tres técnicas distintas para funciones de inserción polimorfas. La versión tercera es una opción viable para situaciones con una jerarquía de clases pequeña en la que se usan más objetos padre que hijos. En estos ejemplos, he asumido que habría muchos más objetos Employee que objetos Manager y Salesperson. DDJ

Noticias relacionadas

Comentarios

No hay comentarios.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

Debes haber iniciado sesión para comentar una noticia.