博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

函数对象

Posted on 2009-08-04 00:47  月光林地  阅读(610)  评论(0编辑  收藏  举报

<effective c++> 第二十条:<宁以pass-by-reference-to-const 替换 pass-by-value> 里说到当参数是内置类型、STL、函数对象时,pass by value 往往比 pass by reference 的效率高。第一次看到函数对象的概念,于是去查了一下google,原来以前接触过的#_#!!!,如callback函数。相关学习信息如下:

先 说明一下函数对象。

 作者:vckbase  VC知识库

    尽管函数指针被广泛用于实现函数回调,但C++还提供了一个重要的实现回调函数的方法,那就是函数对象。函数对象(也称“算符”)是重载了“()”操作符的普通类对象。因此从语法上讲,函数对象与普通的函数行为类似。

用函数对象代替函数指针有几个优点,首先,因为对象可以在内部修改而不用改动外部接口,因此设计更灵活,更富有弹性。函数对象也具备有存储先前调用结果的数据成员。在使用普通函数时需要将先前调用的结果存储在全程或者本地静态变量中,但是全程或者本地静态变量有某些我们不愿意看到的缺陷。

其次,在函数对象中编译器能实现内联调用,从而更进一步增强了性能。这在函数指针中几乎是不可能实现的。

下面举例说明如何定义和使用函数对象。首先,声明一个普通的类并重载“()”操作符:

class Negate

{

public:

int operator() (int n) { return -n;}

};

重载操作语句中,记住第一个圆括弧总是空的,因为它代表重载的操作符名;第二个圆括弧是参数列表。一般在重载操作符时,参数数量是固定的,而重载“()”操作符时有所不同,它可以有任意多个参数。

因为在Negate中内建的操作是一元的(只有一个操作数),重载的“()”操作符也只有一个参数。返回类型与参数类型相同-本例中为int。函数返回与参数符号相反的整数。

使用函数对象

我们现在定义一个叫Callback()的函数来测试函数对象。Callback()有两个参数:一个为int一个是对类Negate的引用。Callback()将函数对象neg作为一个普通的函数名:

#include

using std::cout;

void Callback(int n, Negate & neg)

{

int val = neg(n); //调用重载的操作符“()”

cout << val;

}

不要的代码中,注意neg是对象,而不是函数。编译器将语句

int val = neg(n);

转化为

int val = neg.operator()(n);

通常,函数对象不定义构造函数和析构函数。因此,在创建和销毁过程中就不会发生任何问题。前面曾提到过,编译器能内联重载的操作符代码,所以就避免了与函数调用相关的运行时问题。

为了完成上面个例子,我们用主函数main()实现Callback()的参数传递:

int main()

{

Callback(5, Negate() ); //输出 -5

}

本例传递整数5和一个临时Negate对象到Callback(),然后程序输出-5。

模板函数对象

从上面的例子中可以看出,其数据类型被限制在int,而通用性是函数对象的优势之一,如何创建具有通用性的函数对象呢?方法是使用模板,也就是将重载的操作符“()”定义为类成员模板,以便函数对象适用于任何数据类型:如double,_int64或char:

class GenericNegate

{

public:

template T operator() (T t) const {return -t;}

};

int main()

{

GenericNegate negate;

cout<< negate(5.3333); // double

cout<< negate(10000000000i64); // __int64

}

如果用普通的回调函数实现上述的灵活性是相当困难的。

标准库中函数对象

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 )

}

///////////////////////////////////////////////////////////////////////////////////////////////////////

条款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%,但不要光相信我的话,亲自试试。很容易证实当比较函数对象和真的函数作为算法的参数时,那是一个纯红利。

还有另一个使用函数对象代替函数作为算法参数的理由,而它与效率无关。它涉及到让你的程序可以编译。对于任何理由,STL平台经常完全拒绝有效代码,即使编译器或库或两者都没问题。比如,一个广泛使用的STL平台拒绝了下面(有效的)代码来从cout打印出set中每个字符串的长度:

set<string> s;
...
transform(s.begin(), s.end(),
   ostream_iterator<string::size_type>(cout, "\n"),
   mem_fun_ref(&string::size));
这个问题的原因是这个特定的STL平台在处理const成员函数时有bug(比如string::size)。一个变通方法是改用函数对象:

struct StringSize:
 public unary_function<string, string::size_type>{ // 参见条款40
 string::size_type operator()(const string& s) const
 {
  return s.size();
 }
};

transform(s.begin(), s.end(),
  ostream_iterator<string::size_type>(cout, "\n"),
  StringSize());
解决这问题的还有其他变通办法,但这个不仅在我知道的每个STL平台都可以编译。而且它简化了string::size的内联调用,那是几乎不会在上面把mem_fun_ref(&string::size)传给transform的代码中发生的事情。换句话说,仿函数类StringSize的创造不仅避开了编译器一致性问题,而且可能会带来性能提升。

另一个用函数对象代替函数的原因是它们可以帮助你避免细微的语言陷阱。有时候,看起来合理代码被编译器由于合法性的原因——但很模糊——而拒绝。有很多这样的情况,比如,当一个函数模板实例化的名字不等价于函数名。这是一种这样的情况:

template<typename FPType>     // 返回两个
FPTypeaverage(FPType val1, FPType val2)   // 浮点数的平均值
{
 return (val1 + val2) / 2;
}

template<typename InputIter1,
   typename InputIter2>
void writeAverages(InputIter1 begin1,   // 把两个序列的
    InputIter1 end1,  // 成对的平均值
    InputIter2 begin2,  // 写入一个流
    ostream& s)
{
 transform(
  begin1, end1, begin2,
  ostream_iterator<typename iterator_traits
  <lnputIter1>::value_type> (s, "\n"),
  average<typename iteraror_traits
    <InputIter1>::value_type> // 有错?
 );
}
很多编译器接受这段代码,但是C++标准倾向于禁止它。理由是理论上有可能存在另一带有一个类型参数的函数模板叫做average。如果有,表达式average<typename iterator_traits<lnputIter1>::value_type>将是模棱两可的,因为它不清楚将实例化哪个模板。在这个特殊的例子里,不存在模棱两可,但是无论如何,有些编译器会拒绝这段代码,而且那么做是允许的。没有关系。解决这个问题的方法是依靠一个函数对象:

template<typename FPType>
struct Average:
 public binary_function<FPType, FPType, FPType>{  // 参见条款40
FPType operator()(FPType val1. FPType val2) const
{
 return average(val 1 , val2);
}

template<typename InputIter1, typename InputIter2>
void writeAverages(lnputIter1 begin1, InputIter1 end1,
    InputIter2 begin2, ostream& s)
{
 transform(
  begin1, end1, begin2,
  ostream_iterator<typename iterator_traits<lnputIter1>
      ::value_type>(s, "\n"),
  Average<typename iterator_traits<InputIter1>
    ::value_type>()
 );
}
每个编译器都会接受这条修正的代码。而且在transform里调用Average::operator()符合内联的条件,有些事情对于实例化上面的average不成立,因为averate是一个函数模板,不是函数对象。

把函数对象作为算法的参数所带来的不仅是巨大的效率提升。在让你的代码可以编译方面,它们也更稳健。当然,真函数很有用,但是当涉及有效的STL编程时,函数对象经常更有用。