C++静态虚拟方法的替代方案

40
在C++中,不能声明静态虚函数,也不能将非静态函数转换为C风格函数指针。
现在,我有一个使用函数指针的普通C SDK。
我需要填充一个结构体,其中包含多个函数指针。我计划使用一个具有许多静态纯虚方法的抽象类,并在派生类中重新定义它们并用它们来填充结构体。但直到那时,我才意识到在C++中不允许静态虚函数。
此外,这个C SDK的函数签名没有userData参数。
有什么好的替代方案吗?我能想到的最好的方法是在每个派生类中定义一些纯虚方法GetFuncA()、GetFuncB()等和一些静态成员FuncA()/FuncB(),它们将由GetFuncX()返回。然后,抽象类中的一个函数将调用这些函数以获取指针并填充结构体。
编辑 回答John Dibling的问题,以下操作会很好:
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
}

11
禁止使用静态虚函数是有充分的理由的。如果一个类包含了纯虚函数,那么它就是个抽象类。抽象类无法实例化,所以不存在调用这些函数的风险。但是,如果允许静态虚函数存在,它们可能会在没有实例化该类的情况下被调用。在定义函数之前就调用它们也是毫无防范措施可言的! - Daniel Bingham
18
静态函数的概念与纯虚拟函数的概念是直接相反的。也许如果你解释你想要实现什么,而不是如何实现它,我们可以提供更好的指导。 - John Dibling
1
简单来说:在派生类的构造函数中初始化指针。这并不需要太多额外的代码。 - peterchen
有人能解释一下 ol' 是什么意思吗? - math
@math:这是“旧”的另一种写法。 - raven
显示剩余13条评论
12个回答

24
你可以将Base设置为一个类模板,从其模板参数中获取其函数指针:
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的实例化中下降,使用每个派生类作为模板参数:

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

那种技术被称为“奇异递归模板模式”。如果您忽略在派生类中实现其中一个函数,或者如果您更改了函数签名,则会收到编译错误。这正是您从原始计划中忽略实现其中一个纯虚拟函数时所期望的结果。
然而,这种技术的后果是Derived1和Derived2没有共同的基类。就类型系统而言,两个Base<>的实例没有任何关系。如果您需要它们相关,请引入另一个类来作为模板的基础,然后将共同点放在那里:
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
我想现在我将成为那个挑剔使用静态成员函数作为C回调的可移植性问题的人:https://dev59.com/3XI95IYBdhLWcg3w-DH0 - Georg Fritzsche
当我第一次发布我的答案时,我没有考虑到也许从静态成员函数分配到函数指针的任务只是因为*两者都不是extern C。我仅仅认为如果我的C++编译器接受了这些赋值语句,那么它们至少在我的系统上是兼容的。我已经回过头来将结构体设置为extern C,代码仍然可以工作。在Sun 5.8和GNU 3.4.6和4.1.2上进行了测试。所有三个编译并运行没有警告或错误。 - Rob Kennedy
1
现在它可能在大多数编译器上运行,但我认为至少应该提到它可能会出现问题,而不是暗示使用静态成员函数是完全没有问题的。 - Georg Fritzsche

16

我认为你只需要使用一个普通的虚函数。静态虚函数没有意义,因为虚函数在运行时才能确定。编译器已经完全知道静态函数是什么了,还有什么需要解决的呢?

无论如何,如果可能的话,建议保留现有的函数指针解决方案。否则,考虑使用普通的虚函数。


1
你说的“使用一个普通虚函数”是什么意思?我不能将其强制转换为函数指针,所以…… - raven
2
现在,我有一个使用函数指针的普通C SDK。我必须用几个函数指针填充一个结构体。我打算使用一个抽象类,其中包含一堆静态纯虚方法,并在派生类中重新定义它们并用它们来填充结构体。直到那时,我才意识到C++不允许使用静态虚函数。 - raven
我必须使用几个函数指针填充一个结构体[...]静态纯虚方法[...]在派生类中重新定义它们并用它们填充结构体。 - raven
@Jaime:我没有看到cast这个词,也没有提到将其传递到C回调函数中。你是说你希望派生类使用不同的函数指针。我不指望你接受这个答案,但我想帮助你更好地提问。说出你想做什么(“将虚函数传递给只接受函数指针的C API”),而不是你认为可能能够做到这一点的可能方式(“我想要一个静态虚函数”)。函数是静态的这个事实对我来说完全没有关于你想要做什么的信息。 - Billy ONeal
@Billy:好的,现在我明白发生了什么。当我说“用它们填充结构”时,我真正意思是“将它们放在结构体的函数指针中”,而不是“从它们中填充结构体”。该API是第三方库,所以我不能修改它。如果您仔细阅读所有问题,我可以证明为什么它是隐含的,但现在这样做毫无意义。无论如何,非常感谢您的评论,并将努力记住它们,以便未来提出更好的问题。为了表明我并不怨恨或其他什么,我将接受您的答案。 - raven
显示剩余6条评论

12

我仍然认为静态虚方法有用,这里是一个例子:

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

6
您可以直接将函数传递到基类构造函数中:
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

当将函数指针(回调)传递给C SDK时,常见模式是利用许多这种函数允许的void *参数作为“用户数据”。您可以定义简单的全局函数或静态类成员函数来作为回调。然后,每个回调都可以将“用户数据”参数转换为基类指针,以便调用执行回调工作的成员函数。


遗憾的是,情况并非如此,这个函数的签名没有userData参数。 - raven
@Jaime:如果你在问题中提供了这些事实,我们都不会浪费那么多时间了。 - Georg Fritzsche
我想现在我将成为一个挑剔者,关于使用静态成员函数作为C回调的可移植性问题:https://dev59.com/3XI95IYBdhLWcg3w-DH0 - Georg Fritzsche
再次感谢,gf。没问题,我在这个问题上被Windows卡住了。但这是值得将来记住的事情。 - raven

5

如果一个对象的派生类型可以在编译时确定,你可以使用“奇异递归模板模式”来实现静态多态性。通过这种方法,你不仅可以重载虚拟非静态成员函数,还可以重载静态和非函数成员。你甚至可以重载类型(但基对象大小不能是这些类型的函数)。

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

以下是输出结果:

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

如果派生类型直到运行时才能确定,只需使用虚的非静态成员函数来收集关于类或对象的静态或非函数信息。


4
这些东西肯定很有用-即强制类层次结构中的所有对象都公开一个工厂方法,而不是普通的构造函数。工厂非常有用,可以确保您永远不会构建无效的对象,这是使用普通构造函数无法实现的设计保证。
为了构建“虚拟静态函数”,需要手动将自己的“静态虚函数表”构建到需要它的所有对象中。普通的虚成员函数之所以有效,是因为编译器在所有类实例中构建了一个名为VTABLE的秘密函数指针表。当您构建“T”对象时,该表中的函数指针被分配给提供该API的第一个祖先地址。然后只需用派生类中提供的新函数替换从'new'中得到的对象中的原始指针即可覆盖函数。当然,编译器和运行时都为我们处理这一切。
但是,在现代C++之前的古老时代(据我所知),您必须自己设置此魔法。虚拟静态函数也是如此。好消息是-自己构建的vtable实际上比“普通”的更简单,其条目在任何方面(包括空间和性能)都不比成员函数更昂贵。只需为要支持的API定义一个显式的函数指针集(静态虚函数表)即可定义基类:
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和其他人想要的话,他们仍然可以将这种习惯用法添加到核心语言中。这甚至不是很难。在这样的“C+++”中,每个对象只需要拥有两个vtables - 一个用于接受“this”参数的成员函数,另一个用于普通函数指针。但在此之前,我们还是像旧的C程序员一样被困在手动vtables中。

谢谢!这正是我正在寻找的答案。 - Albert Tomanek

3
假设C SDK允许您将void *传递给数据(并且您应该将派生类的this指针传递给它:)
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的更多详细信息,可能可以提供更相关的建议,但在一般情况下,即使使用静态方法,您仍需要某种方式来获取要使用的“this”指针。

抱歉,没有运气,就像我在Permaquid的答案下面评论的那样 :( - raven

2
明显的方法是在每个派生类中实现FillPointers
class Base
{
private:
    CStruct myStruct;
};

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

然而,您可以通过使用一些模板技巧来避免这种情况...

谢谢,我认为这可能是最好的答案之一,如果它没有遗漏“模板魔法”的细节。 - raven

2
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++ 静态虚拟成员?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接