que - polimorfismo puro c++
Alternativa a los métodos virtuales estáticos de c++ (12)
En C ++ no es posible declarar una función virtual estática, ni emitir una función no estática a un puntero de función de estilo C.
Ahora, tengo un simple SDK C que usa punteros de función en gran medida.
Tengo que completar una estructura con varios indicadores de función. Estaba planeando usar una clase abstracta con un montón de métodos virtuales puros estáticos, y redefinirlos en clases derivadas y llenar la estructura con ellos. No fue hasta entonces que me di cuenta de que estática virtual no está permitida en C ++.
Además, esta firma de función C SDK no tiene un parámetro userData.
¿Hay alguna buena alternativa? Lo mejor que puedo pensar es definir algunos métodos virtuales puros GetFuncA (), GetFuncB (), ... y algunos miembros estáticos FuncA () / FuncB () en cada clase derivada, que serían devueltos por GetFuncX (). Entonces, una función en la clase abstracta llamaría esas funciones para obtener los punteros y llenar la estructura.
Editar Respondiendo a John Dibling, sería genial poder hacer esto:
class Base
{
FillPointers() { myStruct.funA = myFunA; myStruct.funB = myFunB; ...}
private:
CStruct myStruct;
static virtual myFunA(...) = 0;
static virtual myFunB(...) = 0;
};
class Derived1 : public Base
{
Derived1() { FillPointers(); }
static virtual myFunA(...) {...};
static virtual myFunB(...) {...};
};
class Derived2 : public Base
{
Derived2() { FillPointers(); }
static virtual myFunA(...) {...};
static virtual myFunB(...) {...};
};
int main()
{
Derived1 d1;
Derived2 d2;
// Now I have two objects with different functionality
}
Creo que solo necesitas usar una función virtual simple. Una función virtual estática no tiene sentido, porque una función virtual se resuelve en tiempo de ejecución. ¿Qué hay para resolver cuando el compilador sabe exactamente cuál es la función estática?
En cualquier caso, sugiero dejar la solución de puntero de función existente en su lugar si es posible. A excepción de eso, considere usar una función virtual normal.
Estas cosas ciertamente serían útiles, a saber, forzar a todos los objetos en una jerarquía de clases a exponer un método de fábrica en lugar de un constructor ordinario. Las fábricas son muy útiles para garantizar que nunca se generen objetos no válidos, una garantía de diseño que no se puede aplicar casi tan bien con los constructores ordinarios.
Para construir ''estáticas virtuales'', se necesita construir tu propia ''tabla V estática'' a mano en todos los objetos que la necesiten. Las funciones de miembro virtual ordinario funcionan porque el compilador crea una tabla secreta de punteros de función llamada VTABLE en todas las instancias de su clase. Cuando construyes un objeto "T", los punteros a las funciones en esta tabla se asignan a las direcciones del primer ancestro que proporciona esa API. Anulando una función, simplemente se convierte en reemplazar el puntero original en el objeto que obtienes de ''nuevo'' con el nuevo proporcionado en la clase derivada. Por supuesto, el compilador y el tiempo de ejecución manejan todo esto por nosotros.
Pero, en los viejos tiempos antes de la moderna c ++ (según me han dicho), tenías que establecer esta magia por ti mismo. Y ese sigue siendo el caso de la estática virtual. La buena noticia es esta: el vtable que construyes a mano para ellos es en realidad más simple que el ''ordinario'', sus entradas no son más caras de ninguna manera, incluido el espacio y el rendimiento, que las de las funciones de los miembros. Simplemente defina la clase base con un conjunto EXPLÍCITO de punteros a las funciones (el vtable estático) para las API que desea admitir:
class Base {
public:
void Initialize() { /* Pass /this/ and a pointer to myFuncAGate to your C SDK */ }
virtual myFuncA()=0;
// This is the method you pass to the C SDK:
static myFuncAGate(void *user_data) {
((Base*)user_data)->myFuncA();
}
};
class Derived1: public Base {
public:
virtual myFuncA() { ... } // This gets called by myFuncAGate()
};
Ahora, cada objeto que debería soportar un método de fábrica estático puede derivarse de esta clase. Pasan silenciosamente en su propia fábrica a su constructor, y solo agrega 1 puntero al tamaño de los objetos resultantes (al igual que una entrada VTable común).
Strousup y co. aún podría agregar este patrón idiomático al lenguaje central si quisieran. Ni siquiera sería tan difícil. Cada objeto en dicho "C +++" simplemente tendría 2 vtables en lugar de 1-1 para funciones miembro que toman ''this'' como argumento y 1 para punteros de función ordinarios. Hasta ese día, sin embargo, estamos atrapados con vtables manuales al igual que los viejos C-programadores en los días previos a c ++.
La forma obvia es así, con FillPointers
implementados en cada clase derivada.
class Base
{
private:
CStruct myStruct;
};
class Derived1 : public Base
{
private:
static FillPointers() { myStruct.funA = myFunA; myStruct.funB = myFunB; ...}
Derived1() { FillPointers(); }
static myFunA(...) {...};
static myFunB(...) {...};
};
Sin embargo, probablemente puedas evitar eso usando magia de plantilla ...
Las funciones virtuales son esencialmente indicadores de función debajo del capó. Simplemente apuntan a diferentes funciones para diferentes clases. Para simular el comportamiento de la función virtual, tenga un puntero de función almacenado en algún lugar, luego, para ''anular'', simplemente reasignarlo a alguna función diferente.
Alternativamente, es posible que desee probar esto, pero creo que las interfaces tienen una compatibilidad binaria bastante buena. Puede salirse con la suya exponiendo una interfaz C ++ compuesta completamente de funciones virtuales puras, siempre que todos los parámetros y tipos de retorno tengan un formato binario consistente (por ejemplo, tipos C). No es un estándar, pero podría ser lo suficientemente portátil.
Puede hacer que Base
sea una plantilla de clase que tome sus punteros de función de su argumento de plantilla:
extern "C" {
struct CStruct
{
void (*funA)(int, char const*);
int (*funB)(void);
};
}
template <typename T>
class Base
{
public:
CStruct myStruct;
void FillPointers() {
myStruct.funA = &T::myFunA;
myStruct.funB = &T::myFunB;
}
Base() {
FillPointers();
}
};
Luego, defina sus clases derivadas para descender de una instanciación de Base
usando cada clase derivada como argumento de la plantilla:
class Derived1: public Base<Derived1>
{
public:
static void myFunA(int, char const*) { }
static int myFunB() { return 0; }
};
class Derived2: public Base<Derived2>
{
public:
static void myFunA(int, char const*) { }
static int myFunB() { return 1; }
};
int main() {
Derived1 d1;
d1.myStruct.funA(0, 0);
d1.myStruct.funB();
Derived2 d2;
d2.myStruct.funA(0, 0);
d2.myStruct.funB();
}
Esa técnica se conoce como el patrón de plantilla curiosamente recurrente . Si omite implementar una de las funciones en una clase derivada, o si cambia la firma de la función, obtendrá un error de compilación, que es exactamente lo que esperaría obtener si no implementara una de las características virtuales puras. funciones de su plan original.
La consecuencia de esta técnica, sin embargo, es que Derived1
y Derived2
no tienen una clase base común. Las dos instancias de Base<>
no están relacionadas de ninguna manera, en lo que respecta al sistema de tipo. Si necesita que se relacionen, puede introducir otra clase para que sirva de base para la plantilla y luego poner las cosas en común allí:
class RealBase
{
public:
CStruct myStruct;
};
template <typename T>
class Base: public RealBase
{
// ...
};
int main()
RealBase* b;
Derived1 d1;
b = &d1;
b->myStruct.funA(0, 0);
b->myStruct.funB();
Derived2 d2;
b = &d2;
b->myStruct.funA(0, 0);
b->myStruct.funB();
}
Tenga cuidado: las funciones de miembro estático no son necesariamente compatibles con los punteros de función ordinarios. En mi experiencia, si el compilador acepta las declaraciones de asignación que se muestran arriba, entonces al menos puede estar seguro de que son compatibles para ese compilador . Este código no es portátil, pero si funciona en todas las plataformas que necesita admitir, entonces podría considerarlo "suficientemente portátil".
Si el C SDK desea que realice operaciones sin proporcionar datos de usuario, entonces la orientación a objetos probablemente sea innecesaria y usted simplemente debería escribir algunas funciones. De lo contrario, es hora de encontrar un nuevo SDK.
Si el tipo derivado de un objeto puede determinarse en tiempo de compilación, puede utilizar el "Patrón de plantilla curiosamente recurrente" para lograr el polimorfismo estático. Con este enfoque, no está limitado a anular funciones de miembros virtuales no estáticos. Los miembros estáticos y no funcionales son juego limpio. Incluso puede anular los tipos (pero el tamaño del objeto base no puede ser una función de esos tipos).
#include <iostream>
#include <stdint.h>
struct VirtualBase {
static const char* staticConst;
static char* staticVar;
static char* staticFun() { return "original static function"; }
const char* objectConst;
char* objectVar;
virtual char* objectFun() { return "original object function"; }
typedef int8_t Number;
VirtualBase():
objectConst("original object const"),
objectVar("original object var")
{}
void virtual_dump(std::ostream& out=std::cout) {
out << this->staticConst << std::endl;
out << this->staticVar << std::endl;
out << this->staticFun() << std::endl;
out << this->objectConst << std::endl;
out << this->objectVar << std::endl;
out << this->objectFun() << std::endl;
out << "sizeof(Number): " << sizeof(Number) << std::endl;
}
};
const char* VirtualBase::staticConst = "original static const";
char* VirtualBase::staticVar = "original static var";
template <typename Derived>
struct RecurringBase: public VirtualBase {
void recurring_dump(std::ostream& out=std::cout) {
out << Derived::staticConst << std::endl;
out << Derived::staticVar << std::endl;
out << Derived::staticFun() << std::endl;
out << static_cast<Derived*>(this)->staticConst << std::endl;
out << static_cast<Derived*>(this)->staticVar << std::endl;
out << static_cast<Derived*>(this)->staticFun() << std::endl;
out << static_cast<Derived*>(this)->objectConst << std::endl;
out << static_cast<Derived*>(this)->objectVar << std::endl;
out << static_cast<Derived*>(this)->objectFun() << std::endl;
out << "sizeof(Number): " << sizeof(typename Derived::Number) << std::endl;
}
};
struct Defaults : public RecurringBase<Defaults> {
};
struct Overridden : public RecurringBase<Overridden> {
static const char* staticConst;
static char* staticVar;
static char* staticFun() { return "overridden static function"; }
const char* objectConst;
char* objectVar;
char* objectFun() { return "overridden object function"; }
typedef int64_t Number;
Overridden():
objectConst("overridden object const"),
objectVar("overridden object var")
{}
};
const char* Overridden::staticConst = "overridden static const";
char* Overridden::staticVar = "overridden static var";
int main()
{
Defaults defaults;
Overridden overridden;
defaults.virtual_dump(std::cout << "defaults.virtual_dump:/n");
overridden.virtual_dump(std::cout << "overridden.virtual_dump:/n");
defaults.recurring_dump(std::cout << "defaults.recurring_dump:/n");
overridden.recurring_dump(std::cout << "overridden.recurring_dump:/n");
}
Aquí está el resultado:
defaults.virtual_dump:
original static const
original static var
original static function
original object const
original object var
original object function
sizeof(Number): 1
overridden.virtual_dump:
original static const
original static var
original static function
original object const
original object var
overridden object function
sizeof(Number): 1
defaults.recurring_dump:
original static const
original static var
original static function
original static const
original static var
original static function
original object const
original object var
original object function
sizeof(Number): 1
overridden.recurring_dump:
overridden static const
overridden static var
overridden static function
overridden static const
overridden static var
overridden static function
overridden object const
overridden object var
overridden object function
sizeof(Number): 8
Si el tipo derivado no se puede determinar hasta el tiempo de ejecución, simplemente use una función de miembro virtual no estático para reunir información estática o no funcional sobre la clase o el objeto.
Simplemente podría pasar las funciones directamente al constructor de la clase base:
class Base
{
Base()(int (*myFunA)(...), int (*myFunB)(...))
{ myStruct.funA = funA; myStruct.funB = myFunB; ...}
private:
CStruct myStruct;
};
class Derived1 : public Base
{
Derived1() : Base (myFunA, myFunB) {}
static myFunA(...) {...};
static myFunB(...) {...};
};
class Derived2 : public Base
{
Derived2() : Base (myFunA, myFunB) {}
static myFunA(...) {...};
static myFunB(...) {...};
};
int main()
{
Derived1 d1;
Derived2 d2;
// Now I have two objects with different functionality
}
Suponiendo que el C SDK le permite pasarle un vacío * a sus datos (y debe pasarle este puntero a la clase derivada :)
template<typename T>
class VirtualStaticVtable {
private:
typedef T (*StaticFactory)(KnownInputParameters params);
StaticFactory factoryAPI; // The 1 and only entry in my static v-table
protected:
VirtualStaticVtable(StaticFactory factoryApi) : factoryAPI(factoryApi) {}
virtual ~VirtualStaticVtable() {}
};
Si el SDK C no le permite pasar un puntero a sus datos que luego se le devuelve a través de las devoluciones de llamadas, entonces le será muy difícil hacerlo. Como indicaste en uno de tus comentarios que este es el caso, no estás de suerte. Sugiero usar funciones simples como devoluciones de llamada, o sobrecargar el constructor y definir múltiples métodos estáticos. Todavía tendrá dificultades para determinar cuál es el objeto adecuado con el que se supone que deben trabajar sus métodos cuando el código C invoca sus devoluciones de llamada.
Si publica más detalles sobre el SDK, es posible que le brinde sugerencias más relevantes, pero en el caso general, incluso con métodos estáticos, necesita alguna forma de obtener un puntero con el que trabajar.
Todavía puedo ver un uso para métodos virtuales estáticos, aquí un ejemplo:
class File
{
static virtual std::string extension() {return "";}
}
class ExecutableFile : public File
{
// static because every executable has same extension
static virtual std::string extension() {return ".exe";}
}
std::string extension = "";
// needing static
extension = ExecutableFile::extension();
// not needing static nor virtual
ExecutableFile exeFile;
extension = exeFile.extension();
// needing virtual
File* pFile = &exeFile;
extension = pFile->extension();
Un patrón común al pasar un puntero de función (una devolución de llamada) a un SDK de C utiliza el hecho de que muchas de esas funciones permiten un parámetro de vacío * que es "datos de usuario". Puede definir sus devoluciones de llamadas para que sean funciones globales simples o funciones de miembro de clase estática. Luego, cada devolución de llamada puede convertir el parámetro "datos de usuario" a un puntero de clase base para que pueda llamar a una función miembro que hace el trabajo de la devolución de llamada.
class Base
{
template<class T>
FillPointers(T* dummy) { myStruct.funA = T::myFunA; myStruct.funB = T::myFunB; ...}
private:
CStruct myStruct;
};
class Derived1 : public Base
{
Derived1() { FillPointers(this); }
static myFunA(...) {...};
static myFunB(...) {...};
};
class Derived2 : public Base
{
Derived2() { FillPointers(this); }
static myFunA(...) {...};
static myFunB(...) {...};
};
int main()
{
Derived1 d1;
Derived2 d2;
// Now I have two objects with different functionality
}
ver también miembros virtuales estáticos de C ++?