代码之家  ›  专栏  ›  技术社区  ›  doron

虚拟方法或函数指针

  •  30
  • doron  · 技术社区  · 15 年前

    在C++中实现多态行为时,可以使用纯虚方法,也可以使用函数指针(或函子)。例如,异步回调可以通过以下方式实现:

    方法1

    class Callback
    {
    public:
        Callback();
        ~Callback();
        void go();
    protected:
        virtual void doGo() = 0;  
    };
    
    //Constructor and Destructor
    
    void Callback::go()
    {
       doGo();
    }
    

    所以要在这里使用回调,您需要重写dogo()方法来调用您想要的任何函数。

    方法2

    typedef void (CallbackFunction*)(void*)
    
    class Callback
    {
    public:
        Callback(CallbackFunction* func, void* param);
        ~Callback();
        void go();
    private:
       CallbackFunction* iFunc;
       void* iParam;
    };
    
    Callback::Callback(CallbackFunction* func, void* param) :
        iFunc(func),
        iParam(param)
    {}
    
    //Destructor
    
    void go()
    {
        (*iFunc)(iParam);
    }
    

    要在这里使用回调方法,您需要创建一个函数指针,由回调对象调用。

    方法3

    [这是我添加到问题中的(安德烈亚斯);它不是由原来的海报写的]

    template <typename T>
    class Callback
    {
    public:
        Callback() {}
        ~Callback() {}
        void go() {
            T t; t();
        }
    };
    
    class CallbackTest
    {
    public:
        void operator()() { cout << "Test"; }
    };
    
    int main()
    {
        Callback<CallbackTest> test;
    
        test.go();
    }
    

    每个实现的优点和缺点是什么?

    8 回复  |  直到 13 年前
        1
  •  13
  •   Adisak    15 年前

    方法1(虚函数)

    • “+”在C++中的正确方法
    • “-”每个回调必须创建一个新类
    • “-”性能方面,与函数指针相比,通过vf表进行的额外取消引用。与函子解相比的两个间接引用。

    方法2(带函数指针的类)

    • “+”可以为C++回调类包装C样式函数
    • 创建回调对象后,可以更改“+”回调函数
    • “-”需要间接呼叫。对于可以在编译时静态计算的回调,可能比functor方法慢。

    方法3(类调用t函数)

    • “+”可能是最快的方法。无间接呼叫开销,可以完全内联。
    • “-”需要定义其他函数类。
    • “-”要求在编译时静态声明回调。

    fwiw,函数指针与函数指针不同。函数(在C++中)是用来提供函数调用的类,它通常是运算符()。

    下面是一个函数示例以及一个模板函数,它使用了一个函数参数:

    class TFunctor
    {
    public:
        void operator()(const char *charstring)
        {
            printf(charstring);
        }
    };
    
    template<class T> void CallFunctor(T& functor_arg,const char *charstring)
    {
        functor_arg(charstring);
    };
    
    int main()
    {
        TFunctor foo;
        CallFunctor(foo,"hello world\n");
    }
    

    从性能角度来看,虚拟函数和函数指针都会导致间接函数调用(即通过寄存器),尽管虚拟函数在加载函数指针之前需要额外加载vftable指针。使用函数(带有非虚拟调用)作为回调是使用模板函数参数的最高性能方法,因为它们可以是内联的,即使不是内联的,也不会生成间接调用。

        2
  •  6
  •   Andreas Bonini    15 年前

    方法1

    • 更容易阅读和理解
    • 错误可能性较小( iFunc 不能为空,您没有使用 void *iParam
    • C++程序员会告诉你,这是C++中“正确”的方法。

    方法2

    • 稍微少打字
    • 非常 稍微快一点(调用虚方法有一些开销,通常与两个简单的算术运算相同)。所以这很可能无关紧要)
    • 这就是你用C语言做的方法

    方法3

    可能是最好的方法。它将具有最好的性能,它将是类型安全的,并且很容易理解(这是STL使用的方法)。

        3
  •  5
  •   Puppy    13 年前

    方法2的主要问题是它根本无法扩展。考虑100个函数的等效值:

    class MahClass {
        // 100 pointers of various types
    public:
        MahClass() { // set all 100 pointers }
        MahClass(const MahClass& other) {
            // copy all 100 function pointers
        }
    };
    

    MahClass的规模已经膨胀,建造它的时间也显著增加。然而,虚函数是类大小增加的O(1) 构造它的时间——更不用说,用户必须手动编写所有派生类的所有回调,这些派生类将指针调整为指向派生类的指针,并且必须指定函数指针类型和混乱程度。更不用说你可能会忘记一个,或者把它设为空或者其他同样愚蠢但完全会发生的事情,因为你这样写了30个类,就像寄生黄蜂侵犯了毛虫一样侵犯了干燥。

    只有当所需的回调是静态已知的时,方法3才可用。

    这使得方法1成为在需要动态方法调用时唯一可用的方法。

        4
  •  3
  •   NVRAM    15 年前

    从示例中不清楚您是否创建了实用程序类。是你 回调 类的目的是实现一个闭包或一个您刚刚没有充实的更实质的对象?

    第一种形式:

    • 更容易阅读和理解,
    • 更容易扩展:尝试添加方法 暂停,恢复 停止 .
    • 更擅长处理 包封 (假定 多戈 在类中定义)。
    • 可能 更好的抽象,因此更容易维护。

    第二种形式:

    • 可用于不同的方法 多戈 所以它不仅仅是多态的。
    • 允许(使用其他方法)更改 多戈 方法在运行时,允许 实例 创建后改变其功能的对象。

    归根结底,在我看来,第一种形式对所有正常情况都更好。第二种功能有一些有趣的功能,但不是您经常需要的功能。

        5
  •  1
  •   R Samuel Klatchko    15 年前

    第一种方法的一个主要优点是它具有更高的类型安全性。第二个方法对iparam使用void*,因此编译器将无法诊断类型问题。

    第二种方法的一个小优点是与C集成的工作要少一些,但是如果你的代码库仅仅是C++,那么这个优势就没有意义了。

        6
  •  0
  •   Charles    15 年前

    我想说,函数指针更像是C样式的。主要是因为为了使用它们,通常必须定义一个与指针定义具有相同精确签名的平面函数。

    当我写C++时,我所写的唯一的平面函数是int()。其他一切都是类对象。在这两个选项中,我将选择定义一个类并重写您的虚拟机,但是如果您只想通知一些代码您的类中发生了一些操作,那么这两个选项都不是最佳解决方案。

    我不知道你的确切情况,但你可能想细读一下 design patterns

    我建议采用观察者模式。当我需要监视一个类或等待某种通知时,它就是我使用的。

        7
  •  0
  •   Thomas Matthews    15 年前

    例如,让我们看一个用于添加 阅读 类的功能:

    struct Read_Via_Inheritance
    {
       virtual void  read_members(void) = 0;
    };
    

    每当我想添加另一个阅读源时,我必须从类继承并添加一个特定的方法:

    struct Read_Inherited_From_Cin
      : public Read_Via_Inheritance
    {
      void read_members(void)
      {
        cin >> member;
      }
    };
    

    如果我想从文件、数据库或USB中读取,这需要另外3个独立的类。这些组合在多个对象和多个源中开始变得非常难看。

    如果我使用 函数 ,恰好与 访客 设计模式:

    struct Reader_Visitor_Interface
    {
      virtual void read(unsigned int& member) = 0;
      virtual void read(std::string& member) = 0;
    };
    
    struct Read_Client
    {
       void read_members(Reader_Interface & reader)
       {
         reader.read(x);
         reader.read(text);
         return;
       }
       unsigned int x;
       std::string& text;
    };
    

    在上述基础上,对象可以通过提供不同的读取器来从不同的源读取。 read_members 方法:

    struct Read_From_Cin
      : Reader_Visitor_Interface
    {
      void read(unsigned int& value)
      {
         cin>>value;
      }
      void read(std::string& value)
      {
         getline(cin, value);
      }
    };
    

    我不需要更改任何对象的代码(这是一件好事,因为它已经工作了)。我也可以将阅读器应用于其他对象。

    通常,我在执行时使用继承 通用程序设计 .例如,如果我有 Field 类,然后我可以创建 Field_Boolean , Field_Text Field_Integer . In可以将指向其实例的指针放入 vector<Field *> 称之为记录。记录可以对字段执行常规操作,不关心或不知道 友善的 将处理字段的。

        8
  •  0
  •   Charles Eli Cheese    15 年前
    1. 先改为纯虚拟。然后嵌入它。只要内联没有失败(如果强制的话,也不会失败),这就应该否定任何方法开销调用。
    2. 也可以使用C,因为这是C++唯一真正有用的主要特性。C将总是调用方法,并且不能内联,因此它将效率较低。