2010-04-27 7 views
32

En C++ no es posible declarar una función virtual estática, ni echar una función no estática a un puntero de función estilo C.Alternativa a C++ estática métodos virtuales

Ahora, tengo una 'SDK C ol llano que utiliza punteros de función en gran medida.

Tengo que completar una estructura con varios punteros a las funciones. 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++.

También esta firma de la 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 ser capaz de 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 
} 
+10

Hay una buena razón funciones virtuales estáticos no están permitidos. Si tienes una clase que contiene funciones virtuales puras, se vuelve abstracta. No se puede crear una instancia de una clase abstracta, por lo que no hay riesgo de que se invoquen esas funciones. Sin embargo, si permite virtualidades estáticas, podrían ser llamadas desde la clase sin instanciarlo. ¡No habría nada para evitar que se los llamara antes de que se definieran! –

+17

La idea de una función 'estática' se opone directamente a la idea de una función' virtual' pura. Tal vez si usted explica lo que estaba tratando de lograr en lugar de cómo estaba tratando de lograrlo, podríamos darle una mejor orientación. –

+0

¿El SDK C pasa un puntero de contexto a las funciones? Alternativamente, ¿solo necesita tener un manejador activo a la vez? –

Respuesta

20

Puede hacer Base ser una plantilla de clase que tiene 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(); 
    } 
}; 

Entonces , defina sus clases derivadas para descender de una creación de instancias 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 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 los necesita estar relacionada, a continuación, puede introducir otra clase para servir como la base para la plantilla, y luego poner las cosas comunes que 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: funciones miembro estáticas no son necesariamente compatibles con punteros de función ordinarios. En mi experiencia, if 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".

+1

Creo que voy a ser ahora el que nitpicks acerca de los problemas de portabilidad de la utilización de funciones miembro estáticas como C-devoluciones de llamada: http://stackoverflow.com/questions/2068022/in-c-is-it-safe-portable-to-use-static-member- la función de puntero-a-c-api-CALLB –

+0

Cuando por primera vez publicado mi respuesta, que no había considerado que tal vez la asignación de función miembro estática funcione puntero sólo funcionaba para mí porque * no * era 'C' extern. I simplemente pensé que si mi compilador de C++ aceptaba las instrucciones de asignación, entonces eran al menos compatibles en mi sistema. Desde entonces he vuelto e hice la estructura 'extern C', y el código aún funciona. Probado en Sun 5.8 y GNU 3.4 .6 y 4.1.2. Los tres compilan y corren sin advertencias o r errores. –

+1

Podría funcionar en la mayoría de los compiladores ahora, pero creo que al menos debería mencionarse que se puede romper en lugar de implicar que el uso de funciones de miembro estático está perfectamente bien. –

15

Creo que sólo tiene que utilizar una función virtual plano. 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, sugeriría dejando la solución puntero de función existente en el lugar, si es posible. A excepción de eso, considere usar una función virtual normal.

+1

¿Qué quiere decir con "usar una función virtual normal"? No puedo convertirlo en un puntero de función, así que ... – raven

+0

No puede llamar a una función virtual desde el código C, porque C no conoce las tablas de métodos virtuales. –

+0

@Jaime Pardos: A qué función se llama realmente una función virtual, a menos que se conozca el tipo al que llama esa función virtual. Si desea que su función de devolución de llamada sea virtual, lo siento, pero no hay forma de hacerlo. Sin embargo, dado que forzar un lanzamiento a un puntero de función es la razón por la que está usando 'static 'en primer lugar, esto no es una solución de complemento. Pero dado que su pregunta original no contenía esa información, creo que mi interpretación es una respuesta razonable. –

1

Las funciones virtuales son esencialmente punteros 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 bastante buena compatibilidad binaria. 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.

+1

Err ... eso es cierto, pero ¿cómo responde eso la pregunta del OP? :) –

+0

El OP entiende bastante bien qué es una función virtual, y entiende que no es posible hacer lo que quería con ellos. Es por eso que pidió una ** alternativa **. – raven

+0

@Jaime - De acuerdo, disculpa por ser un malvado. Tenía la vaga noción de que cualquiera podría llegar a una solución de trabajo para su problema simplemente entendiendo los principios subyacentes y pensando en ello durante unos minutos. Desde que lo apuñalé y descubrí que estaba equivocado, aquí hay sutilezas que no son obvias. Fui grosero, y me disculpo. Eliminaré el comentario ya que no es útil. –

5

Un patrón común cuando pasa un puntero de función (una devolución de llamada) a una C SDK utiliza el hecho de que muchas de estas funciones permiten un void * parámetro 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.

+0

Tristemente, este no es el caso, esta firma de funciones no tiene un parámetro de datos de usuario. – raven

+0

@Jaime: Si hubiera agregado tales hechos a su pregunta, todos hubiéramos perdido menos tiempo. –

+0

@gf: Lo siento, tienes razón. – raven

2

Suponiendo que el C SDK le permite pasar un void * a sus datos (y debe pasar 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 C doesn' t le permite pasar un puntero a sus datos que luego se le devuelven a través de las devoluciones de llamada, 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 pueda 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.

+0

Lo sentimos, no hay suerte, como lo comenté en virtud de la respuesta de Permaquid :( – raven

4

Se podía pasar las funciones directamente en el 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 
} 
1

La forma más 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 es probable que pueda evitar que el uso de un poco de magia plantilla ...

+0

Gracias, creo que esta sería una de las mejores respuestas, si no hubiera perdido los detalles "mágicos plantilla". – raven

1

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

+0

Estoy atascado con el SDK. Sobre la otra parte de tu respuesta, respuesta interesante. Creo que está mal, sin embargo, en algún sentido de "incorrecto"; la orientación a objetos es SIEMPRE innecesaria, pero sentí que sería interesante encapsular todo en una clase y, teniendo diferentes conjuntos de funciones para diferentes funcionalidades, construir una jerarquía de clases para que todo sea tan fácil de usar y mantener como lo permita mi habilidad. Pensaré acerca de esto. – raven

1
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 C++ static virtual members?

3

Si el tipo derivado de un objeto puede ser determinado en tiempo de compilación, puede utilizar el "patrón de plantilla Curiosamente recurrente" para lograr 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á 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 el tipo derivado no se puede determinar hasta que el tiempo de ejecución, sólo tiene que utilizar una función miembro no estática virtual para recopilar información estática o de no funcionamiento de la clase u objeto.

2

Estas cosas sin duda serían útiles, es decir, obligar a todos los objetos de 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 su propia "tabla V estática" a mano en todos los objetos que lo 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 cree 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 & rendimiento) que las de las funciones miembro. Basta con definir la clase base con un conjunto explícito de los punteros de función (la viable estática) para las API que desea apoyado:

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, todos los objetos que se deben apoyar un método de fábrica estática puede ser derivado 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++.

7

Todavía puedo ver un uso para los 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(); 
Cuestiones relacionadas