C++中的函数对象


参考:C++ Primer Plus 中文版 第六版

很多STL算法都使用函数对象—也叫函数符(functor)。函数符是可以以函数方式与()结合使用的任意对象。这包括函数名、指向函数的指针和重载了()运算符的类对象(即定义了函数 operator())的类)

例如,可以像这样定义一个类:

函数符的概念

正如STL定义了容器和选代器的概念一样,它也定义了函数符概念。

  • 生成器( generator)是不用参数就可以调用的函数符
  • 一元函数( unary function)是用一个参数可以调用的函数符
  • 二元函数( binary function)是用两个参数可以调用的函数符。

例如,提供给 for_each()的函数符应当是一元函数,因为它每次用于一个容器元素当然,这些概念都有相应的改进版:

  • 返回boo值的一元函数是谓词( predicate);
  • 返回booL值的二元函数是二元谓词( binary predicate)

一些STL函数需要谓词参数或二元谓词参数。例如,使用了sort()的这样一个版本,即将二元谓词作为其第3个参数:
bool WorseThan (const Review rl, const Review r2);
sort(books.begin(), books.end(), WorseThan);
list模板有一个将谓词作为参数的 remove_if()成员,该函数将谓词应用于区间中的每个元素,如果谓词返回true,则删除这些元素。
例如,下面的代码删除链表thr中所有大于100的元素
bool toobig(int n){ return n >100; }
list scores;
...
scores.remove_if(toobig);
最后这个例子演示了类函数符适用的地方。假设要删除另一个链表中所有大于200的值。如果能将取值作为第二个参数传递给 toobig(),则可以使用不同的值调用该函数,但谓词只能有一个参数。然而,如果设计一个 Toobig类,则可以使用类成员而不是函数参数来传递额外的信息

template<class T>
class TooBig
{
    T cutoff;
public:
    TooBig(const T& t) : cutoff(t) {}
    bool operator()(const T& val) { return val > cutoff; }
};

这里,一个值(val)作为函数参数传递,而第二个参数(cutoff)是由类构造函数设置的。有了该定义后,就可以将不同的 TooBig 对象初始化为不同的取舍值,供调用 remove_if()时使用。

#include 
#include 
#include 

//函数对象
template<class T>
class TooBig
{
    T cutoff;
public:
    TooBig(const T& t) : cutoff(t) {}
    bool operator()(const T& val) { return val > cutoff; }
};

void outint(int n) {std::cout << n << " ";}

int main()
{
    using std::list;
    using std::cout;
    using std::endl;

    TooBig<int> f100(100);

    //int vals[] = {50,100,90,180,60,210,415,8,188,201};
    //list yadayada(vals, vals+10); // range constructor
    //list etcetera(vals, vals+10);
    //C++11 can use the following instead
    list<int> yadayada={50,100,90,180,60,210,415,88,188,201};
    list<int> etcetera{50,100,90,180,60,210,415,88,108,201};
    cout <<"original lists: \n";
    for_each(yadayada.begin(), yadayada.end(), outint);
    cout << endl;
    for_each(etcetera. begin(), etcetera. end(), outint);
    cout << endl;
    yadayada.remove_if(f100);// use a named function object
    etcetera.remove_if(TooBig<int>(200)); // construct a anonymous function object
    cout <<"Trimmed lists: \n";
    for_each(yadayada.begin(), yadayada.end(), outint);
    cout << endl;
    for_each(etcetera. begin(), etcetera. end(), outint);
    cout << endl;

    return 0;
}
/*
original lists:
50 100 90 180 60 210 415 88 188 201
50 100 90 180 60 210 415 88 108 201
Trimmed lists:
50 100 90 60 88
50 100 90 180 60 88 108
*/
例子 functor

预定义的函数符

#include 

int main()
{
    std::plus<double> add;
    double y = add(2.3, 5.96);
    return 0;
}

它使得将函数对象作为参数很方便:
transform(gr8.begin(), gr8.end(), m8.begin(), out, std::plus());
这里,代码没有创建命名的对象,而是用 plus 构造函数构造了一个函数符,以完成相加运算括号表示调用默认的构造函数,传递给 fransform()的是构造出来的函数对象。对于所有内置的算术运算符、关系运算符和逻辑运算符,STL都提供了等价的函数符。表16.12列出了这些函数符的名称。它们可以用于处理C++内置类型或任何用户定义类型(如果重载了相应的运算符)。

图16.12 运算符和对应的函数符

图16.12 运算符和对应的函数符
运算符 相应的函数符
+ plus
- minus
* multiplies
/ divides
% modulus
- negate
== equal_to
!= not_equal_to
> greater
< less
>= greater_equal
<= less_equal
&& logical_and
|| logical_or
! logical_not

警告:老式C++实现使用函数符名 times,而不是 multiplies

自适应函数符和函数适配器

表16.12列出的预定义函数符都是自适应的。
实际上STL有5个相关的概念:
自适应生成器( adaptablegenerator)、自适应一元函数( adaptable unary function)、自适应二元函数( adaptable-binary function)、自适应谓词( adaptable predicate)和自适应二元谓词( adaptable binary predicate) 使函数符成为自适应的原因是,它携带了标识参数类型和返回类型的 typedef成员。这些成员分别是result_type、 first_argument_type和 second_argument _type,它们的作用是不言自明的。例如,plus对象的返回类型被标识为 plus::result_type,这是int的 typedef
函数符自适应性的意义在于:函数适配器对象可以使用函数对象,并认为存在这些 typedef成员。
例如,接受一个自适应函数符参数的函数可以使用 result_type成员来声明一个与函数的返回类型匹配的变量。
STL提供了使用这些工具的函数适配器类。例如,假设要将矢量gr8的每个元素都增加2.5倍,则需要使用接受一个一元函数参数的 transform()版本,就像前面的例子那样:
transform(gr8.begin(), gr8.end(), out, sqrt)
multiplies()函数符可以执行乘法运行,但它是二元函数。因此需要一个函数适配器,将接受两个参数的函数符转换为接受1个参数的函数符。

前面的 Toobig2示例提供了一种方法,但STL使用 binder1st和binder2nd类自动完成这一过程,它们将自适应二元函数转换为自适应一元函数。

来看 binder1st 。假设有一个自适应二元函数对象f2(),则可以创建一个 binder1st 对象,该对象与一个将被用作 f2() 的第一个参数的特定值(val)相关联 :
binder1st(f2, val) fl;
这样,使用单个参数调用f1(x)时,返回的值与将val作为第一参数、将f1()的参数作为第二参数调用f2(), 返回的值相同。即f1(x)等价于f2(val,x),只是前者是一元函数,而不是二元函数。
f2()函数被适配。同样,仅当f2()是一个自适应函数时,这才能实现。
看上去有点麻烦。然而,STL提供了函数 bind1st(),以简化 binder1st类的使用。可以问其提供用于构建 binder1st对象的函数名称和值,它将返回一个这种类型的对象。
例如,要将二元函数 multiplies()转换为将参数乘以2.5的一元函数,则可以这样做
bind1st(multiplies(), 2.5)
因此,将g8中的每个元素与2.5相乘,并显示结果的代码如下:
transform(gr8.begin(), gr8.end(), out, bind1st(multiplies(), 2.5));
binder2nd类与此类似,只是将常数赋给第二个参数,而不是第一个参数。它有一个名为bind2nd的助手函数,该函数的工作方式类似于 bind1st

程序清单 16.16 将一些最近的示例合并成了一个小程序 :

#ifndef TESTFUNCADAP_H
#define TESTFUNCADAP_H

#include 
#include 
#include 
#include 
#include 

void show(double d) {std::cout.width(6); std::cout << d << ' ';}

class TestFuncAdap
{
    const int LIM = 6;

    public:
        int main()
        {
            using namespace std;
            double arr1[LIM] = {28,29,30,35,38,59};
            double arr2[LIM] = {63,65,69,75,80,99};
            vector<double> gt8(arr1, arr1 + LIM);
            vector<double> m8(arr2, arr2 + LIM);
            cout.setf(ios_base::fixed);
            cout.precision(2);
            cout << "gt8:\t";
            for_each(gt8.begin(), gt8.end(), show);
            cout << endl;
            cout << "m8:\t";
            for_each(m8.begin(), m8.end(), show);
            cout << endl;

            vector<double> sum(LIM);
            transform(gt8.begin(), gt8.end(), m8.begin(), sum.begin(), plus<double>());
            cout << "sum:\t";
            for_each(sum.begin(), sum.end(), show);
            cout << endl;

            vector<double> prod(LIM);
            cout << "prod:\t";
            transform(gt8.begin(), gt8.end(), prod.begin(), bind1st(multiplies<double>(), 2.5) );
            for_each(prod.begin(), prod.end(), show);
            cout << endl;

            /*
gt8:     28.00  29.00  30.00  35.00  38.00  59.00
m8:      63.00  65.00  69.00  75.00  80.00  99.00
sum:     91.00  94.00  99.00 110.00 118.00 158.00
prod:    70.00  72.50  75.00  87.50  95.00 147.50
            */
            return 0;
        }

    protected:

    private:
};

#endif // TESTFUNCADAP_H
程序清单 16.16

C++ 11 提供了函数指针和函数符的替代品 —— Lambda 表达式,将在 18章讨论。

**************

相关