【zz】C++回调函数(callback)与仿函数(functor)的异同

 

C++回调函数(callback)与仿函数(functor)的异同

回调函数(callback)与仿函数(functor)很多时候从用途上来看很相似,以致于我们经常将它们相提并论

。例如:

inline bool compare(int a, int b)
{
 return a > b;
}

struct comparer {
 bool operator()(int a, int b) const {
 return a > b;
 }
};

void main()
{
 std::vector<int> vec, vec2;
 std::sort(vec.begin(), vec.end(), compare);
 std::sort(vec2.begin(), vec2.end(), comparer());
}
仿函数(functor)之所以称为仿函数,是因为这是一种利用某些类对象支持operator()的特性,来达到模

拟函数调用效果的技术。

如果这里vec, vec2这两个vector的内容一样,那么从执行结果看,使用回调函数compare与使用仿函数

comparer是一样的。

那么,我们应该用回调,还是用仿函数?

很多人都说用仿函数吧,回调函数是丑陋的,代码不太象C++风格。

但其实问题的本质不是在代码风格上,仿函数与回调函数各有利弊,不能一概而论。

仿函数(functor)的优点
我的建议是,如果可以用仿函数实现,那么你应该用仿函数,而不要用回调。原因在于:

仿函数可以不带痕迹地传递上下文参数。而回调技术通常使用一个额外的void*参数传递。这也是多数人

认为回调技术丑陋的原因。
更好的性能。
仿函数技术可以获得更好的性能,这点直观来讲比较难以理解。你可能说,回调函数申明为inline了,怎

么会性能比仿函数差?我们这里来分析下。我们假设某个函数func(例如上面的std::sort)调用中传递

了一个回调函数(如上面的compare),那么可以分为两种情况:

func是内联函数,并且比较简单,func调用最终被展开了,那么其中对回调函数的调用也成为一普通函数

调用(而不是通过函数指针的间接调用),并且如果这个回调函数如果简单,那么也可能同时被展开。在

这种情形下,回调函数与仿函数性能相同。
func是非内联函数,或者比较复杂而无法展开(例如上面的std::sort,我们知道它是快速排序,函数因

为存在递归而无法展开)。此时回调函数作为一个函数指针传入,其代码亦无法展开。而仿函数则不同。

虽然func本身复杂不能展开,但是func函数中对仿函数的调用是编译器编译期间就可以确定并进行inline

展开的。因此在这种情形下,仿函数比之于回调函数,有着更好的性能。并且,这种性能优势有时是一种

无可比拟的优势(对于std::sort就是如此,因为元素比较的次数非常巨大,是否可以进行内联展开导致

了一种雪崩效应)。
仿函数(functor)不能做的?
话又说回来了,仿函数并不能完全取代回调函数所有的应用场合。例如,我在std::AutoFreeAlloc中使用

了回调函数,而不是仿函数,这是因为AutoFreeAlloc要容纳异质的析构函数,而不是只支持某一种类的

析构。这和模板(template)不能处理在同一个容器中支持异质类型,是一个道理。

(一)inline函数(摘自C++ Primer的第三版)

 在函数声明或定义中函数返回类型前加上关键字inline即把min()指定为内联。

 inline int min(int first, int secend) {/****/};

 inline函数对编译器而言必须是可见的,以便它能够在调用点内展开该函数。与非inline
函数不同的是,inline函数必须在调用该函数的每个文本文件中定义。当然,对于同一程序
的不同文件,如果inline函数出现的话,其定义必须相同。对于由两个文件compute.C和draw.C构成的程

序来说,程序员不能定义这样的min()函数,它在compute.C中指一件事情,
而在draw.C中指另外一件事情。如果两个定义不相同,程序将会有未定义的行为:

 为保证不会发生这样的事情,建议把inline函数的定义放到头文件中。在每个调用该inline函数的
文件中包含该头文件。这种方法保证对每个inline函数只有一个定义,且程序员无需复制代码,并且
不可能在程序的生命期中引起无意的不匹配的事情。

(二)内联函数的编程风格(摘自高质量C++/C 编程指南)

关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何

作用。

如下风格的函数Foo 不能成为内联函数:
inline void Foo(int x, int y); // inline 仅与函数声明放在一起
void Foo(int x, int y)
{
}
而如下风格的函数Foo 则成为内联函数:
void Foo(int x, int y);
inline void Foo(int x, int y) // inline 与函数定义体放在一起
{
}
所以说,inline 是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。
一般地,用户可以阅读函数的声明,但是看不到函数的定义。尽管在大多数教科书中内
联函数的声明、定义体前面都加了inline 关键字,但我认为inline 不应该出现在函数
的声明中。这个细节虽然不会影响函数的功能,但是体现了高质量C++/C 程序设计风格
的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需
要内联。


定义在类声明之中的成员函数将自动地成为内联函数,例如
class A
{
public:
void Foo(int x, int y) { } // 自动地成为内联函数
}
将成员函数的定义体放在类声明之中虽然能带来书写上的方便,但不是一种良好的编程
风格,上例应该改成:
// 头文件
class A
{
public:
void Foo(int x, int y);
}
// 定义文件
inline void A::Foo(int x, int y)
{
}
慎用内联
内联能提高函数的执行效率,为什么不把所有的函数都定义成内联函数?
如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的
执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收
获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,
消耗更多的内存空间。以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构
函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。
所以不要随便地将构造函数和析构函数的定义体放在类声明中。
一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明
了inline 不应该出现在函数的声明中)。

[1] 内联函数是什么?
内联函数是代码被插入到调用者代码串处的函数。如同 #define 宏,内联函数通过避免被调用的开销来

提高执行效率,尤其是它能够通过调用(“过程化集成”)被编译器优化。

[2] 内联函数是如何在安全和速度上取得折衷?
在 C 中,你可以通过在结构中设置一个 void* 来得到“封装的结构”,在这种情况下,指向实际数据的

void* 指针对于结构的用户来说是未知的。因此结构的用户不知道如何解释void*指针所指内容,但是存

取函数可以将 void* 转换成适当的隐含类型。这样给出了封装的一种形式。

不幸的是这样做丧失了类型安全,并且也将繁琐的对结构中的每个域的访问强加于函数调用。(如果你允

许直接存取结构的域,那么对任何能直接存取的人来说,了解如何解释 void* 指针所指内容就是必要的

了;这样将使改变底层数据结构变的困难)。

虽然函数调用开销是很小的,但它会被累积。C++类允许函数调用以内联展开。这样让你在得到封装的安

全性时,同时得到直接存取的速度。此外,内联函数的参数类型由编译器检查,这是对 C 的 #define 宏

的一个改进。


[3] 为什么我应该用内联函数?而不是原来清晰的 #define 宏?

因为#define宏是在四处是有害的

和 #define 宏不同的是,内联函数总是对参数只精确地进行一次求值,从而避免了声名狼藉的宏错误。

换句话说,调用内联函数和调用正规函数是等价的,差别仅仅是更快:


// 返回 i 的绝对值的宏
#define unsafe(i) /
 ( (i) >= 0 ? (i) : -(i) )

// 返回 i 的绝对值的内联函数
inline
int safe(int i)
{
 return i >= 0 ? i : -i;
}

int f();

void userCode(int x)
{
 int ans;

 ans = unsafe(x++); // 错误!x 被增加两次
 ans = unsafe(f()); // 危险!f()被调用两次

 ans = safe(x++); // 正确! x 被增加一次
 ans = safe(f()); // 正确! f() 被调用一次
}
和宏不同的,还有内联函数的参数类型被检查,并且被正确地进行必要的转换。

宏是有害的;非万不得已不要用。


[4] 如何告诉编译器使非成员函数成为内联函数?
声明内联函数看上去和普通函数非常相似:


void f(int i, char c);
当你定义一个内联函数时,在函数定义前加上 inline 关键字,并且将定义放入头文件:


inline
void f(int i, char c)
{
 // ...
}
注意:将函数的定义({...}之间的部分)放在头文件中是强制的,除非该函数仅仅被单个 .cpp 文件使

用。尤其是,如果你将内联函数的定义放在 .cpp 文件中并且在其他 .cpp文件中调用它,连接器将给出

“unresolved external” 错误。

 

[5] 如何告诉编译器使一个成员函数成为内联函数?
声明内联成员函数看上去和普通函数非常类似:


class Fred {
public:
 void f(int i, char c);
};
但是当你定义内联成员函数时,在成员函数定义前加上 inline 关键字,并且将定义放入头文件中:


inline
void Fred::f(int i, char c)
{
 // ...
}
通常将函数的定义({...}之间的部分)放在头文件中是强制的。如果你将内联函数的定义放在 .cpp 文

件中并且在其他 .cpp 文件中调用它,连接器将给出“unresolved external”错误。

 

[6] 有其它方法告诉编译器使成员函数成为内联吗?
有:在类体内定义成员函数:


class Fred {
public:
 void f(int i, char c)
 {
 // ...
 }
};
尽管这对于写类的人来说很容易,但由于它将类是“什么”(what)和类“如何”(how)工作混在一起,给

阅读的人带来了困难。我们通常更愿意在类体外使用 inline 关键字定义成员函数来避免这种混合。这种

感觉所基于的认识是:在一个面向重用的世界中,使用你的类的人有很多,而建造它的人只有一个(你自

己);因此你做任何事都应该照顾多数而不是少数。


[7] 内联函数保证执行性能更好吗?

不。

小心过度使用内联函数可能导致代码膨胀。在页面调度环境中,它可能会给执行性能带来负面影响。

代码膨胀术语只表示代码的尺寸会增大(膨胀)。在有关内联函数的上下文中,更关心的是内联函数会增

加执行代码的尺寸,并导致操作系统不稳定。这意味着操作系统要花费大部分的时间从磁盘取出代码。

当然,内联函数也可能减小执行代码的尺寸。看上去反了,其实是真的。特别是,调用函数的代码总量有

时会大于展开的内联函数的代码总量。这样的情况会发生于非常小的函数,当优化器能删除很多冗余代码

时——也就是当优化器能使长的函数变短时,也可能会发生于长的函数。

因此结论就是:没有简单的定论。你必须因地制宜。不要使得答案象这样的单纯化,“不要用内联函数”

或“总是使用内联函数”或“当且仅当函数代码少于 N 行时用内联函数”。这种一刀切的方法可能用起

来非常简单,但是它们产生的并不是最佳结果。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值