C++静态虚拟方法的另一种选择

Alternative to c++ static virtual methods

在C++中不可能声明静态虚函数,也不向C样式函数指针抛出非静态函数。

现在,我有了一个普通的ol'c SDK,它大量使用函数指针。

我必须用几个函数指针填充一个结构。我计划将一个抽象类与一系列静态纯虚拟方法一起使用,并在派生类中重新定义它们并用它们填充结构。直到那时我才意识到C++中不允许静态虚拟。

另外,这个c sdks函数签名没有userdata参数。

有什么好的选择吗?我能想到的最好的方法是定义一些纯虚拟方法getfunca()、getfuncb()、…以及每个派生类中的一些静态成员funca()/funcb(),将由getfuncx()返回。然后抽象类中的函数将调用这些函数来获取指针并填充结构。

编辑回答约翰·迪布林的问题,能够做到这一点是很好的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
}


您可以使Base成为一个类模板,它从模板参数中获取函数指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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();
  }
};

然后,将派生类定义为使用每个派生类作为模板参数从Base的实例化中派生:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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();
}

这种技术被称为奇怪的循环模板模式。如果忽略了在派生类中实现某个函数,或者更改了函数签名,则会出现编译错误,如果忽略了从原始计划中实现某个纯虚拟函数,则会出现编译错误。

然而,这种技术的结果是,Derived1Derived2没有共同的基类。就类型系统而言,Base<>的两个实例化没有任何关联。如果您需要将它们关联起来,那么可以引入另一个类作为模板的基础,然后将常见的事情放在那里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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();
}

注意:静态成员函数不一定与普通函数指针兼容。根据我的经验,如果编译器接受上面显示的赋值语句,那么您至少可以确信它们与该编译器兼容。这段代码不可移植,但是如果它可以在所有需要支持的平台上工作,那么您可能会认为它"足够移植"。


我认为您只需要使用一个普通的虚拟函数。静态虚拟函数没有意义,因为虚拟函数是在运行时解析的。当编译器确切知道静态函数是什么时,需要解决什么问题?

在任何情况下,如果可能的话,我建议保留现有的函数指针解决方案。尽管如此,考虑使用一个普通的虚函数。


我仍然可以看到静态虚拟方法的用法,这里有一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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();

向C SDK传递函数指针(回调)时的一个常见模式使用这样一个事实:许多这样的函数都允许一个void*参数,即"用户数据"。您可以将回调定义为简单的全局函数或静态类成员函数。然后,每个回调都可以将"用户数据"参数强制转换为基类指针,这样您就可以调用执行回调工作的成员函数。


如果可以在编译时确定对象的派生类型,则可以使用"奇怪的重复模板模式"来实现静态多态性。使用这种方法,您不仅可以覆盖虚拟的非静态成员函数。静态成员和非功能成员是公平博弈。甚至可以重写类型(但基本对象大小不能是这些类型的函数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#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:
"
);
    overridden.virtual_dump(std::cout <<"overridden.virtual_dump:
"
);
    defaults.recurring_dump(std::cout <<"defaults.recurring_dump:
"
);
    overridden.recurring_dump(std::cout <<"overridden.recurring_dump:
"
);
}

以下是输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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

如果在运行时之前无法确定派生类型,只需使用虚拟非静态成员函数收集有关类或对象的静态或非函数信息。


您可以直接将函数传递给基类构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
}


这些东西当然是有用的——即,强制类层次结构中的所有对象公开工厂方法,而不是普通的构造函数。工厂对于确保您从不构建无效对象非常有用,这是一种设计保证,您几乎不能用普通的构造函数来执行它。

要构建"虚拟静态",需要手工将自己的"静态V表"构建到所有需要它的对象中。普通的虚拟成员函数可以工作,因为编译器在类的所有实例中构建一个称为vtable的函数指针的秘密表。当您构建一个"t"对象时,这个表中的函数指针被分配给提供该API的第一个祖先的地址。重写一个函数就变成用派生类中提供的新指针替换从"new"得到的对象中的原始指针。当然,编译器和运行时为我们处理这一切。

但是,回到现代C++之前的那些旧时代,你必须自己设定这个魔力。对于虚拟静力学来说,情况仍然如此。好消息是,您为它们手工构建的vtable实际上比"普通"vtable简单,它的条目在任何方面都不会比成员函数的条目更昂贵,包括空间和性能。只需使用一组显式的函数指针(静态vtable)为您想要支持的API定义基类:

1
2
3
4
5
6
7
8
9
10
11
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() {}
};

现在,应该支持静态工厂方法的每个对象都可以从此类派生。它们悄悄地将自己的工厂传递给自己的构造函数,并且只向生成的对象的大小添加一个指针(就像普通vtable条目一样)。

如果愿意的话,strousup和co.仍然可以将这种惯用模式添加到核心语言中。不会那么难的。这样的"C++"中的每个对象只需2个VTABLE而不是1个- 1个成员函数,以"这个"为参数,而普通函数指针则为1个。然而,直到那天,我们还是习惯了手动的VTABLE,就像老C程序员在C++之前的日子一样。


假设C SDK允许您将一个void*传递给您的数据(并且您应该将派生类的这个指针传递给它:)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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()
};

如果C SDK不允许您传递指向数据的指针,然后通过回调将其传递给您,那么您将很难做到这一点。既然你在你的评论中指出事实确实如此,你就不太走运了。我建议使用简单函数作为回调,或者重载构造函数并定义多个静态方法。当您的回调被C代码调用时,您仍然很难确定您的方法应该使用的正确对象是什么。

如果您发布有关SDK的更多详细信息,可能会给您提供更多相关的建议,但在一般情况下,即使使用静态方法,您也需要某种方法来获取此指针以进行工作。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
}

还可以看到C++静态虚拟成员吗?


如果C SDK希望您在不提供用户数据的情况下执行操作,那么对象定向可能是不必要的,您应该只编写一些函数。否则,是时候找到一个新的SDK了。


显而易见的方法是这样的,在每个派生类中实现FillPointers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base
{
private:
    CStruct myStruct;
};

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

不过,您可能可以避免使用一些模板魔术…


虚函数本质上是引擎盖下的函数指针。它们只是针对不同的类指向不同的函数。要模拟虚拟函数行为,请将函数指针存储在某个位置,然后要"重写",只需将其重新分配给其他函数即可。

或者,您可能想测试这个,但是我认为接口具有非常好的二进制兼容性。只要所有的参数和返回类型都具有一致的二进制格式(例如C类型),就可以暴露出完全由纯虚函数组成的C++接口。这不是一个标准,但它可能足够便携。