【zz】C++回调函数(callback)与仿函数(functor)的异同
2010-04-23 15:29
351 查看
http://hi.baidu.com/laodun/blog/item/ffcc8f82e2108694f603a696.html
http://hi.baidu.com/ruo0ruo/blog/item/19525bca54d4f314bf09e653.html
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)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。 类的 b138 构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构 函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。 所以不要随便地将构造函数和析构函数的定义体放在类声明中。 一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明 了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 行时用内联函数”。这种一刀切的方法可能用起 来非常简单,但是它们产生的并不是最佳结果。 |
相关文章推荐
- C++回调函数(callback)与仿函数(functor)的异同
- C++回调函数(callback)与仿函数(functor)的异同
- C++回调函数(callback)与仿函数(functor)的异同
- C++回调函数(callback)与仿函数(functor)的异同
- C++回调函数(callback)与仿函数(functor)的异同
- C++回调函数(callback)与仿函数(functor)的异同
- C++回调函数(callback)与仿函数(functor)的异同
- C++回调函数(callback)与仿函数(functor)的异同
- C++回调函数(callback)与仿函数(functor)的异同
- C++回调函数(callback)与仿函数(functor)的异同
- C++回调函数(callback)与仿函数(functor)的异同
- C++回调函数(callback)与仿函数(functor)的异同
- C++回调函数 (callback)与仿函数(functor)的异同
- c++回调函数 callback
- c++回调函数 callback
- C++中回调函数(CALLBACK)初探
- C++回调函数(callback)的使用
- c++回调函数 callback
- 【重学C/C++】回调函数callback
- C++中回调函数(CallBack)的使用