33

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

方法一

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

//Constructor and Destructor

void Callback::go()
{
   doGo();
}

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

方法二

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

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

4

8 回答 8

14

方法一(虚函数)

  • "+" 正确的 C++ 方法
  • "-" 必须为每个回调创建一个新类
  • “-” 与函数指针相比​​,通过 VF 表在性能方面的额外取消引用。与 Functor 解决方案相比的两个间接参考。

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

  • "+" 可以为 C++ 回调类包装一个 C 风格的函数
  • "+" 创建回调对象后可以更改回调函数
  • "-" 需要间接调用。对于可以在编译时静态计算的回调,可能比仿函数方法慢。

方法 3(类调用 T 函子)

  • "+" 可能是最快的方法。没有间接调用开销,并且可以完全内联。
  • "-" 需要定义一个额外的 Functor 类。
  • "-" 要求在编译时静态声明回调。

FWIW,函数指针与函子不同。函子(在 C++ 中)是用于提供通常是 operator() 的函数调用的类。

这是一个示例仿函数以及一个使用仿函数参数的模板函数:

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 指针。使用 Functor(带有非虚拟调用)作为回调是使用参数到模板函数的性能最高的方法,因为它们可以内联,即使没有内联,也不会生成间接调用。

于 2009-12-23T20:26:07.180 回答
7

方法一

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

方法二

  • 打字少一点
  • 非常快(调用虚拟方法有一些开销,通常与两个简单的算术运算相同。所以很可能没关系)
  • 这就是你在 C 中的做法

方法 3

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

于 2009-12-23T20:26:40.360 回答
5

方法 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) 增加类的大小构造它的时间 - 更不用说你,用户,必须手动为所有派生类编写所有回调,调整指针成为一个指向派生的指针,而且必须指定函数指针类型和什么乱七八糟的。更不用说您可能会忘记一个,或者将其设置为 NULL 或其他同样愚蠢但完全会发生的想法,因为您正在以这种方式编写 30 个类并且违反 DRY 就像寄生黄蜂违反毛虫一样。

方法 3 仅在所需回调静态可知时可用。

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

于 2012-06-22T18:00:45.117 回答
3

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

第一种形式:

  • 更容易阅读和理解,
  • 更容易扩展:尝试添加方法pause、resumestop
  • 更擅长处理封装(假设doGo在类中定义)。
  • 可能是一个更好的抽象,所以更容易维护。

第二种形式:

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

最终,IMO,第一种形式对所有正常情况都更好。不过,第二个有一些有趣的功能——但不是你经常需要的。

于 2009-12-23T20:39:17.520 回答
1

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

第二种方法的一个次要优点是与 C 集成的工作量更少。但是如果您的代码库只有 C++,那么这个优势就没有实际意义。

于 2009-12-23T20:36:49.803 回答
0

我会说函数指针更像是 C 风格。主要是因为为了使用它们,您通常必须定义一个具有与指针定义完全相同的签名的平面函数。

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

我不知道您的确切情况,但您可能想仔细阅读设计模式

我建议观察者模式。当我需要监视课程或等待某种通知时,我会使用它。

于 2009-12-23T20:28:39.090 回答
0

例如,让我们看一个向类添加读取功能的接口:

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 个单独的类。多个对象和多个来源的组合开始变得非常丑陋。

如果我使用functor,它恰好类似于Visitor设计模式:

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_TextField_Integer. In 可以将指向它们的实例的指针放入 a 中vector<Field *>并将其称为记录。记录可以对字段进行通用操作,不关心也不知道处理的是什么类型的字段。

于 2009-12-23T20:41:42.380 回答
0
  1. 改成纯虚拟,先关。然后内联它。这应该完全否定任何方法开销调用,只要内联不会失败(如果你强制它不会失败)。
  2. 不如用C,因为这是C++相比C唯一真正有用的大特性。你会一直调用方法,不能内联,效率会低一些。
于 2009-12-23T22:14:19.710 回答