函数对象
函数对象是重载了“()”操作符的普通类对象。因此从语法上讲,函数对象与普通的函数行为类似。用函数对象代替函数指针有几个优点:
1)首先,因为对象可以在内部修改而不用改动外部接口,因此设计更灵活,更富有弹性。函数对象也具备有存储先前调用结果的数据成员。在使用普通函数时需要将先前调用的结果存储在全程或者本地静态变量中,但是全程或者本地静态变量有某些我们不愿意看到的缺陷。
2)其次,在函数对象中编译器能实现内联调用,从而更进一步增强了性能。这在函数指针中几乎是不可能实现的。
3)函数对象属于类对象,可以把函数设计为模板函数。如
#define LENGTH(s) (sizeof(s)/sizeof(s[0]))
template<class T>
class dump{
public:
void operator()(T& arg){cout<<arg<<"\t";}
};
int main( int argc, char **argv )
{
vector<string> vec_string;
……
for_each(vec_string.begin(),vec_string.end(),dump<string>());
return 0;
}
这一点常常用在泛型算法的参数。因为在泛型算法里,经常没有办法确定要处理的数据是什么类型的,因此需要把它的处理函数设计成模板。
内联优势举例:
条款46:考虑使用函数对象代替函数作算法的参数
一个关于用高级语言编程的抱怨是抽象层次越高,产生的代码效率就越低。事实上,Alexander Stepanov(STL的发明者)有一次作了一组小测试,试图测量出相对C,C++的“抽象惩罚”。其中,测试结果显示基本上操作包含一个double的类产生的代码效率比对应的直接操作一个double的代码低。因此你可能会奇怪地发现把STL函数对象——化装成函数的对象——传递给算法所产生的代码一般比传递真的函数高效。
比如,假设你需要以降序排序一个double的vector。最直接的STL方式是通过sort算法和greater<double>类型的函数对象:
vector<double> v;
...
sort(v.begin(), v.end(), greater<double>());
如果你注意了抽象惩罚,你可能决定避开函数对象而使用真的函数,一个不光是真的,而且是内联的函数:
inline
bool doubleGreater(double d1, double d2)
{
return dl > d2;
}
...
sort(v.begin(), v.end(), doubleGreater);
有趣的是,如果你比较了两个sort调用(一个使用greater<double>,一个使用doubleGreater)的性能,你基本上可以明确地知道使用greater<double>的那个更快。实际来说,我在四个不同的STL平台上测量了对一百万个double的vector的两个sort调用,每个都设为针对速度优化,使用greater<double>的版本每次都比较快。最差的情况下,快50%,最好的快了160%。抽象惩罚真多啊。
这个行为的解释很简单:内联。如果一个函数对象的operator()函数被声明为内联(不管显式地通过inline或者隐式地通过定义在它的类定义中),编译器就可以获得那个函数的函数体,而且大部分编译器喜欢在调用算法的模板实例化时内联那个函数。在上面的例子中,greater<double>::operator()是一个内联函数,所以编译器在实例化sort时内联展开它。结果,sort没有包含一次函数调用,而且编译器可以对这个没有调用操作的代码进行其他情况下不经常进行的优化。(内联和编译器优化之间交互的讨论,参见《Effective C++》的条款33和Bulka与Mayhew的《Efficient C++》[10]的第8-10章。)
使用doubleGreater调用sort的情况则不同。要知道有什么不同,我们必须知道不可能把一个函数作为参数传给另一个函数。当我们试图把一个函数作为参数时,编译器默默地把函数转化为一个指向那个函数的指针,而那个指针是我们真正传递的。因此,这个调用
sort(v.begin(), v.end(), doubleGreater);不是真的把doubleGreater传给sort,它传了一个doubleGreater的指针。当sort模板实例化时,这是产生函数的声明:
void sort(vector<double>::iterator first, // 区间起点
vector<double>::iterator last, // 区间终点
bool (*comp)(double, double)); // 比较函数
因为comp是一个指向函数的指针,每次在sort中用到时,编译器产生一个间接函数调用——通过指针调用。大部分编译器不会试图去内联通过函数指针调用的函数,甚至,正如本例中,那个函数已经声明为inline而且这个优化看起来很直接。为什么不?可能因为编译器厂商从来没有觉得值得实现这个优化。你得稍微同情一下编译器厂商。他们有很多需求,而他们不能做每一件事。你的需要并不能让他们实现那个优化。
把函数指针作为参数会抑制内联的事实解释了一个长期使用C的程序员经常发现却难以相信的现象:在速度上,C++的sort实际上总是使C的qsort感到窘迫。当然,C++有函数、实例化的类模板和看起来很有趣的operator()函数需要调用,而C只是进行简单的函数调用,但所有的C++“开销”都在编译期被吸收。在运行期,sort内联调用它的比较函数(假设比较函数已经被声明为inline而且它的函数体在编译期可以得到)而qsort通过一个指针调用它的比较函数。结果是sort运行得更快。在我的测试的一百万个double的vector上,它快670%,但不要光相信我的话,亲自试试。很容易证实当比较函数对象和真的函数作为算法的参数时,那是一个纯红利。
【标准库中的函数对象 】
C++标准库定义了几个有用的函数对象,它们可以被放到STL算法中。例如,sort()算法以判断对象(predicate object)作为其第三个参数。判断对象是一个返回Boolean型结果的模板化的函数对象。可以向sort()传递greater<>或者less<>来强行实现排序的升序或降序:
#include // for greater<> and less<>
#include //for sort()
#include
using namespace std;
int main()
{
vector vi;
//..填充向量
sort(vi.begin(), vi.end(), greater() );//降序( descending )
sort(vi.begin(), vi.end(), less() ); //升序 ( ascending )
}