herencia - ejemplos sencillos de polimorfismo en c++
llamar a la función virtual pura desde el constructor de la clase base (5)
Tengo una clase base MyBase que contiene una función virtual pura:
void PrintStartMessage() = 0
Quiero que cada clase derivada lo llame en su constructor
luego lo puse en el constructor de la clase base ( MyBase
)
class MyBase
{
public:
virtual void PrintStartMessage() =0;
MyBase()
{
PrintStartMessage();
}
};
class Derived:public MyBase
{
public:
void PrintStartMessage(){
}
};
void main()
{
Derived derived;
}
pero obtengo un error de enlazador.
this is error message :
1>------ Build started: Project: s1, Configuration: Debug Win32 ------
1>Compiling...
1>s1.cpp
1>Linking...
1>s1.obj : error LNK2019: unresolved external symbol "public: virtual void __thiscall MyBase::PrintStartMessage(void)" (?PrintStartMessage@MyBase@@UAEXXZ) referenced in function "public: __thiscall MyBase::MyBase(void)" (??0MyBase@@QAE@XZ)
1>C:/Users/Shmuelian/Documents/Visual Studio 2008/Projects/s1/Debug/s1.exe : fatal error LNK1120: 1 unresolved externals
1>s1 - 2 error(s), 0 warning(s)
Quiero forzar a todas las clases derivadas a ...
A- implement it
B- call it in their constructor
¿Cómo debo hacerlo?
Hay muchos artículos que explican por qué nunca debe llamar funciones virtuales en constructor y destructor en C ++. Mire here y here para obtener más información sobre lo que ocurre detrás de la escena durante esas llamadas.
En resumen, los objetos se construyen desde la base hasta la derivada. Por lo tanto, cuando intenta llamar a una función virtual desde el constructor de la clase base, la anulación de las clases derivadas aún no se ha producido debido a que los constructores derivados aún no han sido llamados.
Lo más cerca que puede llegar a hacer algo así es construir completamente su objeto primero y luego llamar al método después:
template <typename T>
T construct_and_print()
{
T obj;
obj.PrintStartMessage();
return obj;
}
int main()
{
Derived derived = construct_and_print<Derived>();
}
Tratar de llamar a un método abstracto puro desde un derivado mientras ese objeto aún se está construyendo es inseguro. Es como tratar de llenar de gasolina un automóvil, pero ese automóvil todavía está en la línea de ensamblaje y aún no se ha instalado el tanque de gasolina. Quiero decir, ¿qué diablos esperas que pase?
No debe llamar a una función virtual
en un constructor. Period Tendrá que encontrar alguna solución, como hacer que PrintStartMessage
no sea virtual
y poner la llamada explícitamente en cada constructor.
No puede hacerlo de la forma que imagina porque no puede llamar a funciones virtuales derivadas desde el constructor de la clase base; el objeto aún no es del tipo derivado. Pero no necesitas hacer esto.
Llamar a PrintStartMessage después de la construcción de MyBase
Supongamos que quieres hacer algo como esto:
class MyBase {
public:
virtual void PrintStartMessage() = 0;
MyBase() {
printf("Doing MyBase initialization.../n");
PrintStartMessage(); // ⚠ UB: pure virtual function call ⚠
}
};
class Derived : public MyBase {
public:
virtual void PrintStartMessage() { printf("Starting Derived!!!/n"); }
};
El rastro de ejecución deseado sería:
Doing MyBase initialization...
Starting Derived!!!
¡Pero esto es para lo que son los constructores! ¡Deseche la función virtual y haga que el constructor del derivado haga el trabajo!
class MyBase {
public:
MyBase() { printf("Doing MyBase initialization.../n"); }
};
class Derived : public MyBase {
public:
Derived() { printf("Starting Derived!!!/n"); }
};
La salida es, bueno, lo que esperaríamos:
Doing MyBase initialization...
Starting Derived!!!
Sin embargo, esto no aplica las clases derivadas para implementar explícitamente la funcionalidad PrintStartMessage
. Pero, por otro lado, piense dos veces si es necesario, ya que de lo contrario siempre pueden proporcionar una implementación vacía de todos modos.
Llamar a PrintStartMessage antes de la construcción de MyBase
Como se dijo anteriormente, si va a llamar a PrintStartMessage
antes de que Derived
haya sido un constructor, no podrá lograr esto porque aún no hay un objeto Derived
para PrintStartMessage
. No tendría sentido exigir que PrintStartMessage
sea un miembro no estático porque no tendría acceso a ninguno de los miembros de datos Derived
.
Una función estática con función de fábrica
Alternativamente podemos hacer que sea un miembro estático como ese:
class MyBase {
public:
MyBase() {
printf("Doing MyBase initialization.../n");
}
};
class Derived : public MyBase {
public:
static void PrintStartMessage() { printf("Derived specific message./n"); }
};
Una pregunta natural surge de cómo se llamará?
Hay dos soluciones que puedo ver: una es similar a la de @greatwolf, donde tienes que llamarla manualmente. Pero ahora, dado que es un miembro estático, puede llamarlo antes de que se haya construido una instancia de MyBase
:
template<class T>
T print_and_construct() {
T::PrintStartMessage();
return T();
}
int main() {
Derived derived = print_and_construct<Derived>();
}
La salida será
Derived specific message.
Doing MyBase initialization...
Este enfoque obliga a todas las clases derivadas a implementar PrintStartMessage
. Lamentablemente, solo es cierto cuando los construimos con nuestra función de fábrica ... que es una gran desventaja de esta solución.
La segunda solución es recurrir al patrón de plantilla curiosamente recurrente (CRTP). Al decirle a MyBase
el tipo de objeto completo en tiempo de compilación, puede hacer la llamada desde el constructor:
template<class T>
class MyBase {
public:
MyBase() {
T::PrintStartMessage();
printf("Doing MyBase initialization.../n");
}
};
class Derived : public MyBase<Derived> {
public:
static void PrintStartMessage() { printf("Derived specific message./n"); }
};
El resultado es el esperado, sin la necesidad de utilizar una función de fábrica dedicada.
Accediendo a MyBase desde PrintStartMessage con CRTP
Cuando MyBase
está ejecutando, ya está bien para acceder a sus miembros. Podemos hacer que PrintStartMessage
pueda acceder al MyBase
que lo ha llamado:
template<class T>
class MyBase {
public:
MyBase() {
T::PrintStartMessage(this);
printf("Doing MyBase initialization.../n");
}
};
class Derived : public MyBase<Derived> {
public:
static void PrintStartMessage(MyBase<Derived> *p) {
// We can access p here
printf("Derived specific message./n");
}
};
Lo siguiente también es válido y se usa con mucha frecuencia, aunque es un poco peligroso:
template<class T>
class MyBase {
public:
MyBase() {
static_cast<T*>(this)->PrintStartMessage();
printf("Doing MyBase initialization.../n");
}
};
class Derived : public MyBase<Derived> {
public:
void PrintStartMessage() {
// We can access *this member functions here, but only those from MyBase
// or those of Derived who follow this same restriction. I.e. no
// Derived data members access as they have not yet been constructed.
printf("Derived specific message./n");
}
};
Sin plantillas, solución, rediseño
Otra opción más es rediseñar un poco tu código. IMO esta es en realidad la solución preferida si tiene que llamar a un PrintStartMessage
anulado desde la construcción de MyBase
.
Esta propuesta consiste en separar Derived
from MyBase
, de la siguiente manera:
class ICanPrintStartMessage {
public:
virtual ~ICanPrintStartMessage() {}
virtual void PrintStartMessage() = 0;
};
class MyBase {
public:
MyBase(ICanPrintStartMessage *p) : _p(p) {
_p->PrintStartMessage();
printf("Doing MyBase initialization.../n");
}
ICanPrintStartMessage *_p;
};
class Derived : public ICanPrintStartMessage {
public:
virtual void PrintStartMessage() { printf("Starting Derived!!!/n"); }
};
Inicializas MyBase
siguiente manera:
int main() {
Derived d;
MyBase b(&d);
}
Si PrintStartMessage () no era una función virtual pura sino una función virtual normal, el compilador no se quejaría de ello. Sin embargo, todavía tendría que averiguar por qué no se está llamando a la versión derivada de PrintStartMessage ().
Como la clase derivada llama al constructor de la clase base antes que su propio constructor, la clase derivada se comporta como la clase base y, por lo tanto, llama a la función de la clase base.