虚拟方法或功能指针

时间:2009-12-23 20:17:52

标签: c++ function-pointers virtual-functions

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

要在此处使用回调方法,您需要创建一个由Callback对象调用的函数指针。

方法3

[这是我(Andreas)提出的问题;它不是由原始海报写的]

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 个答案:

答案 0 :(得分:13)

方法1(虚拟功能)

  • “+”“在C ++中执行此操作的正确方法
  • “ -​​ ”每个回调必须创建一个新类
  • “ -​​ ”性能方面,通过VF-Table与功能指针进行额外的解除引用。与Functor解决方案相比,两个间接引用。

方法2(带功能指针的类)

  • “+”可以为C ++ Callback Class包装C风格的函数
  • “+”回调对象创建后可以更改回调函数
  • “ -​​ ”需要间接通话。对于可以在编译时静态计算的回调,可能比functor方法慢。

方法3(班级调用T函子)

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

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

答案 1 :(得分:6)

方法1

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

方法2

  • 打字的次数要少一些
  • 非常稍快一些(调用虚方法有一些开销,通常是两个简单的算术运算相同..所以它很可能无关紧要)
  • 这就是你在C
  • 中的表现

方法3

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

答案 2 :(得分: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个类并且像寄生蜂一样违反了干扰毛虫。

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

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

答案 3 :(得分:3)

从您的示例中可以看出,您是否正在创建实用程序类。你是回调类是为了实现一个你没有充实的闭包或更实质的对象吗?

第一种形式:

  • 更容易阅读和理解,
  • 更容易扩展:尝试添加方法暂停,恢复停止
  • 更擅长处理封装(假设 doGo 在课程中定义)。
  • 可能是更好的抽象,更容易维护。

第二种形式:

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

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

答案 4 :(得分:1)

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

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

答案 5 :(得分:0)

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

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

我不知道您的具体情况,但您可能想要仔细阅读design patterns

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

答案 6 :(得分:0)

例如,让我们看一下将 read 功能添加到类中的接口:

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_BooleanField_TextField_Integer。可以将指向其实例的指针放入vector<Field *>并将其称为记录。该记录可以对字段执行泛型操作,而不关心或知道字段的处理。

答案 7 :(得分:0)

  1. 首先改为纯虚拟。然后内联它。这应该取消任何方法开销调用,只要内联不会失败(如果强制它就不会失败)。
  2. 也可以使用C,因为与C相比,这是C ++唯一真正有用的主要特性。你总是会调用方法,它不能内联,因此效率会降低。