Alternativa a los métodos virtuales estáticos de c++


En C++ no es posible declarar una función virtual estática, ni enviar una función no estática a un puntero de función de estilo C.

Ahora, tengo un SDK de C simple que usa punteros de función en gran medida.

Tengo que llenar una estructura con varios punteros 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 la estática virtual no están permitidos en C++.

También esta firma de función C SDKs no tiene un parámetro de datos de usuario.

Hay alguna buena alternativa? Lo mejor que se me ocurre 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 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
}
Author: Georg Fritzsche, 2010-04-27

12 answers

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 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 curiosamente recurrente patrón de plantilla. Si descuida 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 se olvidara de implementar una de las funciones virtuales puras 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 tipos. Si necesita que estén relacionados, puede introducir otra clase que sirva como base para la plantilla, y luego poner las cosas comunes hay:

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();
}

Cuidado: Las funciones miembro estáticas no son necesariamente compatibles con los punteros de función ordinarios. En mi experiencia, si el compilador acepta las instrucciones de asignación mostradas anteriormente, entonces puede al menos estar seguro de que son compatibles para ese compilador. Este código no es portable, pero si funciona en todas las plataformas que necesitas soportar, entonces podrías considerarlo "lo suficientemente portable."

 20
Author: Rob Kennedy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-04-28 18:16:14

Creo que solo necesita 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 que resolver cuando el compilador sabe exactamente qué es la función estática?

En cualquier caso, sugeriría dejar la solución de puntero de función existente en su lugar si es posible. Dejando eso al descubierto, considere usar una función virtual normal.

 15
Author: Billy ONeal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-04-27 14:08:24

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();
 7
Author: Eno,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-05 16:40:24

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 estas funciones permiten un parámetro void * que es "datos de usuario". Puede definir sus devoluciones de llamada para que sean funciones globales simples o funciones miembro de clase estáticas. Luego, cada devolución de llamada puede enviar 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.

 5
Author: Permaquid,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-04-27 14:22:05

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
}
 4
Author: Eclipse,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-04-27 14:59:49

Si el tipo derivado de un objeto se puede determinar en tiempo de compilación, puede usar el "Patrón de Plantilla Curiosamente recurrente" para lograr polimorfismo estático. Con este enfoque no se limita a anular funciones miembro virtuales no estáticas. Los miembros estáticos y no funcionales son un juego limpio. Incluso puede anular 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á la salida:

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 no se puede determinar el tipo derivado hasta el tiempo de ejecución, simplemente use una función miembro virtual no estática para recopilar información estática o no funcional sobre la clase u objeto.

 3
Author: Eric Mahurin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-01-31 22:11:40

Estas cosas ciertamente serían útiles, es decir, forzar a todos los objetos en una jerarquía de clases a exponer un método factory en lugar de un constructor ordinario. Las fábricas son muy útiles para asegurarse de que nunca construya objetos inválidos, una garantía de diseño que no puede hacer cumplir tan bien con constructores ordinarios.

Para construir 'estática virtual' requiere construir su propia "tabla v estática" a mano en todos los objetos que la necesitan. Las funciones miembro virtuales ordinarias funcionan porque el compilador construye una tabla secreta de punteros de función llamados VTABLE en todas las instancias de su clase. Cuando se crea un objeto "T", los punteros de función de esta tabla se asignan a las direcciones del antepasado 1st que proporciona esa API. Sobreescribir una función entonces simplemente se convierte en reemplazar el puntero original en el objeto que obtiene de ' new ' 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 del c++ moderno (según me han dicho), tenías que configurar esta magia tú mismo. Y ese sigue siendo el caso de la estática virtual. La buena noticia es esta: la vtable que construyes a mano para ellos es en realidad más simple que la 'ordinaria', sus entradas no son más caras de ninguna manera, incluido el espacio y el rendimiento, que las de las funciones miembro. Simplemente defina la clase base con un conjunto EXPLÍCITO de punteros de función (la vtable estática) para las API que desea admitir:

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() {}
};

Ahora, cada objeto que debe soportar un método de fábrica estática se puede derivar de esta clase. Pasan silenciosamente en su propia fábrica a su constructor, y solo agrega 1 puntero a los tamaños de los objetos resultantes (al igual que una entrada VTable ordinaria).

Strousup and co. aún podrían agregar este patrón idiomático al lenguaje principal si quisieran. Ni siquiera sería tan difícil. Cada objeto en tal "C+++" tendría simplemente 2 vtables en lugar de 1 - 1 para las funciones miembro tomando 'esto' como argumento y 1 para punteros de función ordinarios. Hasta ese día, sin embargo, estamos atascados con vtables manuales al igual que los viejos programadores de C en los días anteriores a c++.

 3
Author: Zack Yezek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-19 05:29:14

Asumiendo que el SDK de C le permite pasarle un void * a sus datos (y debería pasarle su este puntero para la clase derivada:)

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()
};

Si el SDK de C no le permite pasar un puntero a sus datos que luego se le devuelve a través de las devoluciones de llamada, entonces tendrá un tiempo muy difícil haciendo esto. Ya que usted indicó en uno de sus comentarios que este es el caso, está prácticamente sin suerte. Yo sugeriría el uso de funciones simples como devoluciones de llamada, o sobrecarga del constructor y definición de 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 sus devoluciones de llamada son invocadas por el código C.

Si publica más detalles sobre el SDK, podría ser posible darle sugerencias más relevantes, pero en el caso general, incluso con métodos estáticos, necesita alguna forma de obtener un este puntero para trabajar.

 2
Author: Ori Pessach,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-04-27 15:22:42

Las funciones virtuales son esencialmente punteros de función bajo el capó. Solo 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 'override' simplemente reasignarlo a alguna función diferente.

Alternativamente, es posible que desee probar esto, pero creo que las interfaces tienen bastante buena compatibilidad binaria. Podría salirse con la suya exponiendo una interfaz de C++ compuesta completamente de funciones virtuales puras, siempre y cuando todos los parámetros y tipos de retorno tengan un formato binario consistente (ej. Tipos C). No es un estándar, pero podría ser lo suficientemente portátil.

 1
Author: AshleysBrain,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-04-27 14:20:56

La forma obvia es así, con FillPointers implementado 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 puede evitar que el uso de un poco de magia plantilla...

 1
Author: Roddy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-04-27 15:05:53

Si el SDK de C quiere que realice operaciones sin proporcionar datos de usuario, entonces la orientación a objetos es probablemente innecesaria y solo debe escribir algunas funciones. De lo contrario, es hora de encontrar un nuevo SDK.

 1
Author: Puppy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-04-27 16:28:37
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
}

Véase también ¿Miembros virtuales estáticos de C++?

 1
Author: Alsk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:10:31