【问题标题】:Alternative to c++ static virtual methodsc++ 静态虚拟方法的替代方案
【发布时间】:2011-02-12 21:23:42
【问题描述】:

在 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
}

【问题讨论】:

  • 不允许使用静态虚函数是有充分理由的。如果你有一个包含纯虚函数的类,它就会变成抽象的。抽象类无法实例化,因此不存在调用这些函数的风险。但是,如果您允许静态虚拟,它们可能会在不实例化的情况下从类中调用。没有什么可以阻止它们在定义之前被调用!
  • static 函数的想法与纯virtual 函数的想法直接相反。也许,如果您解释了您想要实现的目标,而不是您如何尝试实现它,我们可能会为您提供更好的指导。
  • C SDK 是否将上下文指针传递给函数?或者,您一次只需要激活一个处理程序吗?
  • @Jamie:你能发布一些你喜欢写但不能因为没有static virtual这样的东西而不能写的代码吗?
  • 简单:初始化派生类的CTor中的指针。实际上并没有那么多额外的代码。

标签: c++ static virtual function-pointers


【解决方案1】:

你可以让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();
}

这种技术被称为奇怪的重复模板模式。如果您忽略在派生类中实现其中一个函数,或者如果您更改了函数签名,您将得到一个编译错误,这正是您在忽略实现纯虚函数之一时所期望得到的原计划中的功能。

然而,这种技术的结果是Derived1Derived2 没有共同的基类。就类型系统而言,Base&lt;&gt; 的两个实例没有任何关系。如果你需要它们相关,那么你可以引入另一个类作为模板的基础,然后把共同的东西放在那里:

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

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

【讨论】:

  • 我想我现在将成为使用静态成员函数作为 C 回调的可移植性问题的挑剔者:stackoverflow.com/questions/2068022/…
  • 当我第一次发布我的答案时,我没有考虑到也许从静态成员函数到函数指针的分配只对我有用,因为 都不 不是extern C。我只是想,如果我的 C++ 编译器接受了赋值语句,那么它们至少在我的系统上是兼容的。我已经回去制作了结构extern C,并且代码仍然有效。在 Sun 5.8 和 GNU 3.4.6 和 4.1.2 上测试。所有这三个编译和运行没有警告或错误。
  • 它现在可能适用于大多数编译器,但我认为至少应该提到它可以中断而不是暗示使用静态成员函数非常好。
【解决方案2】:

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

无论如何,我建议尽可能保留现有的函数指针解决方案。抛开这一点,考虑使用普通的虚函数。

【讨论】:

  • “使用普通的虚函数”是什么意思?我不能将它转换为函数指针,所以...
  • 你不能从 C 代码中调用虚函数,因为 C 不知道虚方法表。
  • @Jaime Pardos:不知道虚函数实际调用的函数,除非知道该虚函数调用的类型。如果您希望您的回调函数是虚拟的,对不起,但没有办法做到这一点。鉴于强制强制转换为函数指针是您首先使用static 的原因,但这不是插件解决方案。但鉴于您最初的问题未包含该信息,我认为我的解释是合理的回答。
  • "[...]现在,我有一个普通的 C SDK,它大量使用函数指针。我必须用几个函数指针填充一个结构。我打算使用具有一堆静态纯虚拟方法的抽象类,并在派生类中重新定义它们并用它们填充结构。直到那时我才意识到静态虚拟在 C++ 中是不允许的。” 我会说得很清楚。
  • @Jaime:我看不到将虚函数 传入 C sdk 的任何内容,也看不到将这些静态虚函数转换为函数指针的任何内容。
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

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

    【讨论】:

    • 很遗憾,情况并非如此,此函数签名没有 userData 参数。
    • @Jaime:如果你在你的问题中添加了这样的事实,我们都会浪费更少的时间。
    • 我想我现在将成为使用静态成员函数作为 C 回调的可移植性问题的挑剔者:stackoverflow.com/questions/2068022/…
    • 再次感谢您,gf。没问题,我被窗户困住了。但这是未来值得牢记的事情。
    【解决方案5】:

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

    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】:

      如果可以在编译时确定对象的派生类型,则可以使用“Curiously Recurring Template Pattern”来实现静态多态。使用这种方法,您不仅限于覆盖虚拟非静态成员函数。静态成员和非函数成员是公平的游戏。您甚至可以覆盖类型(但基础对象大小不能是这些类型的函数)。

      #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
      

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

      【讨论】:

        【解决方案7】:

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

        要构建“虚拟静态”,需要手动将您自己的“静态 v-table”构建到所有需要它的对象中。普通的虚成员函数之所以起作用,是因为编译器在类的所有实例中构建了一个名为 VTABLE 的函数指针秘密表。当您构建“T”对象时,此表中的函数指针被分配给提供该 API 的第一个祖先的地址。重写一个函数然后简单地变成用派生类中提供的新指针替换你从“新”获得的对象中的原始指针。当然,编译器和运行时会为我们处理这一切。

        但是,在现代 c++ 之前的真正旧时代(有人告诉我),您必须自己设置这个魔法。虚拟静力学仍然如此。好消息是——你为它们手动构建的 vtable 实际上比“普通”的更简单,它的条目在任何方面都不会比成员函数更昂贵——包括空间和性能。只需使用一组 EXPLICIT 函数指针(静态 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() {}
        };
        

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

        Strousup 和公司。如果他们愿意,仍然可以将这种惯用模式添加到核心语言中。甚至不会那么难。这样的“C++”中的每个对象都将简单地具有 2 个 vtable,而不是 1-1 用于将 'this' 作为参数的成员函数和 1 用于普通函数指针。然而,直到那一天,我们仍然坚持使用手动 vtable,就像在 c++ 之前的那些日子里,老 C 程序员一样。

        【讨论】:

        • 谢谢!正是我正在寻找的答案。
        【解决方案8】:

        假设 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 不允许您传递指向数据的指针,然后通过回调将其传回给您,那么您将很难做到这一点。由于您在您的一个 cmets 中表示确实是这种情况,因此您非常不走运。我建议使用简单的函数作为回调,或者重载构造函数并定义多个静态方法。当您的回调被 C 代码调用时,您仍然很难确定您的方法应该使用的正确对象是什么。

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

        【讨论】:

        • 抱歉,运气不好,正如我在 Permaquid 的回答下评论的那样:(
        【解决方案9】:

        虚拟函数本质上是底层的函数指针。它们只是指向不同类的不同功能。要模拟虚函数行为,请将函数指针存储在某处,然后“覆盖”它,只需将其重新分配给某个不同的函数即可。

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

        【讨论】:

        • Err...这是真的,但这如何回答 OP 的问题? :)
        • OP 非常了解虚函数是什么,并且明白不可能用它们做他想做的事。这就是他要求替代的原因。
        • @Jaime - 好的,很抱歉被嘲讽了。我有一个模糊的、半生不熟的想法,任何人只要了解基本原理并思考几分钟,就可以为你的问题找到可行的解决方案。从那以后,我自己尝试了一下,发现我错了——这里有一些不明显的微妙之处。我很粗鲁,我道歉。我会删除评论,因为它没有帮助。
        • 没问题,奥里。但是,看不到删除评论的好处,但仍然...
        【解决方案10】:

        显而易见的方式是这样的,在每个派生类中实现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(...) {...};
        };
        

        但是你可以使用一些模板魔法来避免这种情况......

        【讨论】:

        • 谢谢,如果没有错过“模板魔术”的详细信息,我认为这将是最好的答案之一。
        【解决方案11】:

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

        【讨论】:

        • 我被 SDK 卡住了。关于你答案的另一部分,有趣的答案。然而,在某种“错误”的意义上,我认为这是错误的;面向对象总是不必要的,但我觉得将所有内容封装在一个类中会很有趣,并且为不同的功能提供不同的函数集,构建一个类层次结构,以使所有内容都尽可能易于使用和维护,只要我的技能允许。我会考虑的。
        【解决方案12】:
        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++ static virtual members?

        【讨论】:

          猜你喜欢
          • 2013-03-26
          • 2012-10-11
          • 2010-11-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多