web-dev-qa-db-fra.com

Alternative aux méthodes virtuelles statiques c ++

En C++, il n'est pas possible de déclarer une fonction virtuelle statique, ni de convertir une fonction non statique en un pointeur de fonction de style C.

Maintenant, j'ai un SDK ol 'C ordinaire qui utilise fortement les pointeurs de fonction.

Je dois remplir une structure avec plusieurs pointeurs de fonction. Je prévoyais d'utiliser une classe abstraite avec un tas de méthodes virtuelles pures statiques, de les redéfinir dans des classes dérivées et d'en remplir la structure. Ce n'est qu'alors que j'ai réalisé que le virtuel statique n'est pas autorisé en C++.

De plus, cette signature de fonction C SDKs n'a pas de paramètre userData.

Y a-t-il une bonne alternative? Le mieux que je puisse penser est de définir des méthodes virtuelles pures GetFuncA (), GetFuncB (), ... et certains membres statiques FuncA ()/FuncB () dans chaque classe dérivée, qui seraient retournés par GetFuncX (). Une fonction de la classe abstraite appelle alors ces fonctions pour obtenir les pointeurs et remplir la structure.

Edit En répondant à John Dibling, ce serait formidable de pouvoir faire ceci:

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
}
36
raven

Vous pouvez faire de Base un modèle de classe qui prend ses pointeurs de fonction à partir de son argument de modèle:

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

Ensuite, définissez vos classes dérivées pour descendre d'une instanciation de Base en utilisant chaque classe dérivée comme argument de modèle:

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

Cette technique est connue sous le nom de modèle de modèle curieusement récurrent . Si vous négligez d'implémenter l'une des fonctions dans une classe dérivée, ou si vous modifiez la signature de la fonction, vous obtiendrez une erreur de compilation, qui est exactement ce que vous attendez si vous négligez d'implémenter l'une des fonctions virtuelles pures fonctions de votre plan d'origine.

Cependant, la conséquence de cette technique est que Derived1 et Derived2 n'ont pas de classe de base commune. Les deux instanciations de Base<> ne sont liés d'aucune façon, en ce qui concerne le système de types. Si vous avez besoin qu'ils soient liés, vous pouvez introduire une autre classe pour servir de base au modèle, puis y mettre les éléments communs:

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

Attention: Les fonctions membres statiques ne sont pas nécessairement compatibles avec les pointeurs de fonction ordinaires. D'après mon expérience, si le compilateur accepte les instructions d'affectation ci-dessus, alors vous pouvez au moins être sûr qu'elles sont compatibles pour ce compilateur . Ce code n'est pas portable, mais s'il fonctionne sur toutes les plates-formes que vous devez prendre en charge, vous pouvez le considérer comme "suffisamment portable".

23
Rob Kennedy

Je pense que vous avez juste besoin d'utiliser une simple fonction virtuelle. Une fonction virtuelle statique n'a pas de sens, car une fonction virtuelle est résolue au moment de l'exécution. Qu'y a-t-il à résoudre lorsque le compilateur sait exactement ce qu'est la fonction statique?

Dans tous les cas, je suggère de laisser la solution de pointeur de fonction existante en place si possible. À part cela, envisagez d'utiliser une fonction virtuelle normale.

16
Billy ONeal

Un modèle courant lors du passage d'un pointeur de fonction (un rappel) à un SDK C utilise le fait que de nombreuses fonctions de ce type autorisent un paramètre void * qui est des "données utilisateur". Vous pouvez définir vos rappels comme des fonctions globales simples ou des fonctions membres de classe statiques. Ensuite, chaque rappel peut convertir le paramètre "données utilisateur" en un pointeur de classe de base afin que vous puissiez appeler une fonction membre qui effectue le travail de rappel.

6
Permaquid

Vous pouvez simplement passer les fonctions directement dans le constructeur de la classe de 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
}
6
Eclipse

Si le type dérivé d'un objet peut être déterminé au moment de la compilation, vous pouvez utiliser le "Modèle de modèle curieusement récurrent" pour obtenir un polymorphisme statique. Avec cette approche, vous n'êtes pas limité à simplement remplacer les fonctions membres non statiques virtuelles. Les membres statiques et non fonctionnels sont un jeu équitable. Vous pouvez même remplacer les types (mais la taille de l'objet de base ne peut pas être fonction de ces types).

#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");
}

Voici la sortie:

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 le type dérivé ne peut pas être déterminé avant l'exécution, utilisez simplement une fonction membre non statique virtuelle pour collecter des informations statiques ou non fonctionnelles sur la classe ou l'objet.

5
Eric Mahurin

Ces choses seraient certainement utiles, à savoir pour forcer tous les objets d'une hiérarchie de classes à exposer une méthode d'usine au lieu d'un constructeur ordinaire. Les usines sont très utiles pour vous assurer de ne jamais construire d'objets invalides, une garantie de conception que vous ne pouvez pas appliquer aussi bien avec des constructeurs ordinaires.

Pour construire des "statiques virtuelles", il faut construire à la main votre propre "table en v statique" dans tous les objets qui en ont besoin. Les fonctions membres virtuelles ordinaires fonctionnent parce que le compilateur crée une table secrète de pointeurs de fonction appelée VTABLE dans toutes les instances de votre classe. Lorsque vous créez un objet "T", les pointeurs de fonction de ce tableau sont affectés aux adresses du premier ancêtre fournissant cette API. Remplacer une fonction devient alors simplement remplacer le pointeur d'origine dans l'objet que vous obtenez de "nouveau" par le nouveau fourni dans la classe dérivée. Bien sûr, le compilateur et le runtime gèrent tout cela pour nous.

Mais, à l'époque très ancienne du c ++ moderne (c'est ce qu'on me dit), vous avez dû configurer cette magie vous-même. Et c'est toujours le cas pour la statique virtuelle. La bonne nouvelle est la suivante: la table que vous construisez à la main pour eux est en fait plus simple que la `` ordinaire '', ses entrées ne sont en aucun cas plus coûteuses, y compris l'espace et les performances, que celles des fonctions membres. Définissez simplement la classe de base avec un ensemble EXPLICIT de pointeurs de fonction (la table statique) pour les API que vous souhaitez prendre en charge:

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

Désormais, chaque objet devant prendre en charge une méthode d'usine statique peut être dérivé de cette classe. Ils passent tranquillement dans leur propre usine à leur constructeur, et cela n'ajoute qu'un pointeur à la taille des objets résultants (tout comme une entrée VTable ordinaire).

Strousup et co. pourrait encore ajouter ce modèle idiomatique au langage de base s'ils le souhaitaient. Ce ne serait même pas si difficile. Chaque objet dans un tel "C+++" aurait simplement 2 tables vtables au lieu de 1 pour les fonctions membres prenant "ceci" comme argument et 1 pour les pointeurs de fonctions ordinaires. Jusqu'à ce jour, cependant, nous sommes coincés avec des vtables manuelles, tout comme les anciens programmeurs C l'étaient avant C++.

4
Zack Yezek

En supposant que le SDK C vous permet de lui passer un void * à vos données (et vous devriez lui passer votre pointeur this pour la classe dérivée :)

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 le SDK C ne vous permet pas de passer un pointeur vers vos données qui vous sont ensuite renvoyées via les rappels, vous aurez vraiment du mal à le faire. Puisque vous avez indiqué dans l'un de vos commentaires que c'est effectivement le cas, vous n'avez pas de chance. Je suggérerais d'utiliser des fonctions simples comme rappels, ou de surcharger le constructeur et de définir plusieurs méthodes statiques. Vous aurez toujours du mal à déterminer quel est l'objet approprié avec lequel vos méthodes sont censées fonctionner lorsque vos rappels sont invoqués par le code C.

Si vous publiez plus de détails sur le SDK, il peut être possible de vous donner des suggestions plus pertinentes, mais dans le cas général, même avec des méthodes statiques, vous avez besoin d'un moyen d'obtenir un pointeur this avec lequel travailler.

3
Ori Pessach

La manière la plus évidente est la suivante, avec FillPointers implémenté dans chaque classe dérivée.

class Base
{
private:
    CStruct myStruct;
};

class Derived1 : public Base
{
 private:
    static FillPointers() { myStruct.funA = myFunA; myStruct.funB = myFunB; ...}
    Derived1() {  FillPointers();  }
    static myFunA(...) {...};
    static myFunB(...) {...};
};

Cependant, vous pouvez probablement éviter cela en utilisant un peu de magie de modèle ...

2
Roddy
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
}

voir aussi membres virtuels statiques C++?

2
Alsk

Les fonctions virtuelles sont essentiellement des pointeurs de fonction sous le capot. Ils indiquent simplement différentes fonctions pour différentes classes. Pour simuler le comportement d'une fonction virtuelle, ayez un pointeur de fonction stocké quelque part, puis pour le "remplacer", il suffit de le réaffecter à une fonction différente.

Alternativement, vous voudrez peut-être tester cela, mais je pense que les interfaces ont une assez bonne compatibilité binaire. Vous pourriez vous en sortir en exposant une interface C++ entièrement composée de fonctions virtuelles pures, tant que tous les paramètres et types de retour ont un format binaire cohérent (par exemple, les types C). Ce n'est pas une norme, mais il pourrait être suffisamment portable.

2
AshleysBrain

Si le SDK C veut que vous effectuiez des opérations sans fournir de données utilisateur, alors l'orientation objet n'est probablement pas nécessaire et vous devez simplement écrire certaines fonctions. Sinon, il est temps de trouver un nouveau SDK.

2
Puppy