在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);
}
要在此处使用回调方法,您需要创建一个由Callback对象调用的函数指针。
[这是我(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();
}
每项实施的优缺点是什么?
答案 0 :(得分:13)
方法1(虚拟功能)
方法2(带功能指针的类)
方法3(班级调用T函子)
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)
iFunc
不能为NULL,您没有使用void *iParam
等可能是最好的方法。它将具有最佳性能,它将是类型安全的,并且易于理解(这是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)
从您的示例中可以看出,您是否正在创建实用程序类。你是回调类是为了实现一个你没有充实的闭包或更实质的对象吗?
第一种形式:
第二种形式:
最终,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_Boolean
,Field_Text
和Field_Integer
。可以将指向其实例的指针放入vector<Field *>
并将其称为记录。该记录可以对字段执行泛型操作,而不关心或知道字段的种处理。
答案 7 :(得分:0)