《C++ Primer Plus》第16章:string类和标准模板库(9)

函数对象

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

class Linear{
private:
	double slope;
	double y0;
public:
	Linear(double s1_ = 1, double y_ = 0)
		: slope(s1_), y0(y_) {}
	double operator() (double x) { return y0 + slope * x; }
}

这样,重载的()运算符将使得能够像函数那样使用 Linear 对象:

Linear f1;
Linear f2(2.5, 10.0);
double y1 = f1(12.5);	// right-hand side is f1.operator()(12.5);
double y2 = f2(0.4);

其中,y1 将使用表达式 0 + 1 * 12.5 来计算,y2 将使用表达式 10.0 + 2.5 * 0.4 来计算。在表达式 y0 + slope * x 中,y0 和 slope 的值来自对象的构造函数,而 x 的值来自 operator()() 的参数。

还记得函数 for_each 吗?它将指定的函数用于区间中的每个成员:

for_each(books.begin(), books.end(), ShowReview);

通常,第3个参数可以是常规函数,也可以是函数符。实际上,这提出了一个问题:如何声明第3个参数呢?不能把它声明为函数指针,因为函数指针指定了参数类型。由于容器可以包含任意类型,所以预先无法知道应使用哪种参数类型。STL 通过使用模板解决了这个问题。for_each 的原型看上去就像这样:

template<class InputIterator, class Function>
Function for_each(InputIterator first, InputIterator last, Function f);

ShowReview() 的原型如下:

void ShowReview(const Review &);

这样,标识符 ShowReview 的类型将为 void(*)(const Review &),这也是赋给模板参数 Function 的类型。对于不同的函数调用,Function 参数可以表示具有重载() 运算符的类类型。最终,for_each() 代码将具有一个使用 f() 的表达式。在 ShowReview() 示例中,f 是指向函数的指针,而 f() 调用该函数。如果最后的 for_each() 参数是一个对象,则 f() 将是调用其重载的 () 运算符的对象。

函数符概念

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

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

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

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

一些STL函数需要谓词参数或二元谓词参数。例如,之前有一个程序使用了 sort() 的这样一个版本,即将二元谓词作为其第3个参数:

bool WorseThan(const Review & r1, const Review & r2);
...
sort(books.begin(), books.end(), WorseThan);

list 模板有一个将谓词作为参数的 remove_if() 成员,该函数将谓词应用于区间中的每个元素,如果谓词返回 true,则删除这些元素。例如,下面的代码删除链表three中所有大于100的元素:

bool tooBig(int n) { return n > 100; }
list<int> scores;
...
scores.remove_if(tooBig);

最后这个例子演示了类函数符使用的地方。假设要删除另一个链表中中所有大于200的值。如果能将取舍值作为第二个参数传递给 tooBig(),则可以使用不同的值调用该函数,但谓词只能有一个参数。然而,如果设计一个 TooBig 类,则可以使用类成员而不是函数参数来传递额外的信息:

tempalte<class T>
class TooBig{
private:
	T cutoff;
public:
	TooBig(const T & t) : cutoff(t) {}
	bool operator() (const T & v) { return v > cutoff; }
};

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

// functor.cpp -- using a functor
#include <iostream>
#include <list>
#include <iterator>
#include <algorithm>

template<class T> // functor class defines operator()()
class TooBig{
private:
    T cutoff;
public:
    TooBig(const T & t) : cutoff(t) {}
    bool operator()(const T & v) { return v > cutoff; }
};

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

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

    TooBig<int> f100(100);  // limit = 100
    int vals[10] = {50, 100, 90, 180, 60, 210, 415, 88, 188, 201};
    list<int> yadayada(vals, vals+10); // range constructor
    list<int> etcetera(vals, vals+10);

    // C++11 cna 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, 188, 201};
    cout << "Original lists:\n";
    for_each(yadayada.begin(), yadayada.end(), outint);
    cout << endl;
    std::for_each(etcetera.begin(), etcetera.end(), outint);
    cout << endl;
    yadayada.remove_if(f100);
    etcetera.remove_if( TooBig<int> (200));

    cout << "Trimmed lists:\n";
    for_each(yadayada.begin(), yadayada.end(), outint);
    cout << endl;
    for_each(etcetera.begin(), etcetera.end(), outint);
    cout << endl;

    return 0;
}

一个函数符(f100) 是一个声明的对象,而另一个函数符(TooBig<int>(200)) 是一个匿名对象,它是由构造函数调用创建的。下面是程序的输出:

Original lists:
50 100 90 180 60 210 415 88 188 201
50 100 90 180 60 210 415 88 188 201
Trimmed lists:
50 100 90 60 88
50 100 90 180 60 88 188

假设已经有了一个接受两个参数的模板函数:

template<class T>
bool tooBig(const T & val, const T & lim) {
	return val > lim;
}

则可以使用类将它转换为单个参数的函数对象:

template<class T>
class TooBig2{
private:
	T cutoff;
public:
	TooBig2(const T & t) : cutoff(t) {}
	bool operator() (const T & v) { return tooBig<T>(v, cutoff); }
};

即可以这样做:

TooBig2<int> tB100(100);
int x;
cin >> x;
if(tB100(x)) // same as if (tooBig(x, 100))
	...

因此,调用tB100(x) 相当于调用tooBig(x, 100),但两个参数的函数被转换为单参数的函数对象,其中第二个参数被用于构建函数对象。简而言之,类函数符 TooBig2 是一个函数适配器,使函数能够满足不同的接口。

在该程序清单中,可使用 C++11 的初始化列表功能来简化初始化。为此,可将如下代码:

int vals[10] = {50, 100, 90, 180, 60, 210, 415, 88, 188, 201 };
list<int> yadayada(vals, vals + 10); // range constructor
list<int> etcetera(vals, vals+10);

替换为如下代码:

list<int> yadayada = {50, 100, 90, 180, 60, 210, 415, 88, 188, 201};
list<int> etcetera {50, 100, 90, 180, 60, 210, 415, 88, 188, 201};

预定义的函数符

STL 定义了多个基本函数符,它们执行诸如将两个值相加、比较两个值是否相等操作。提供这些函数对象是为了支持将函数作为参数的 STL 函数。例如,考虑函数 transform()。它有两个版本。第一个版本接受 4 个参数,前两个参数是指定容器区间的迭代器(现在您应该已熟悉了这种方法),第 3 个参数是指定将结果复制到哪里的迭代器,最后一个参数是一个函数符,它被应用于区间中的每个元素,生成结果中的新元素。例如,请看下面的代码:

const int LIM = 5;
double arr1[LIM] = { 36, 39, 42, 45, 48 };
vector<double> gr8(arr1, arr1+LIM);
ostream_iterator<double, char> out(out, " ");
transform(gr8.begin(), gr8.end(), out, sqrt);

上述代码计算每个元素的平方根,并将结果发送到输出流。目标迭代器可以位于原始区间中。例如,将上述示例中的 out 替换为 gr8.begin() 后,新值将覆盖原来的值。很明显,使用的函数符必须是接受单个参数的函数符。

第2中版本使用一个接受两个参数的函数,并将该函数用于两个区间中元素。它用另一个参数(即第3个)标识第二个区间的起始位置。例如,如果 m8 是另一个 vector<double> 对象,mean(double,double) 返回两个值的平均值,则下面的代码将输出来自 gr8 和 m8 的值的平均值:

transform(gr8.begin(), gr8.end9), m8.begin(), out, mean);

现在假设要将两个数组相加。不能将+作为参数,因为对于类型double来说,+是内置的运算符,而不是函数。可以定义一个将两个数相加的函数,然后使用它:

double add(double x, double y) { return x + y; }
...
transform(gr8.begin(), gr8.end(), m8.begin(), out, add};

然而,这样必须为每种类型单独定义一个函数。更好的办法是定义一个模板(除非 STL 已经有一个模板了,这样就不必定义)。头文件 functional(以前为 function.h)定义了多个模板类函数对象,其中包括 plus<>()。

可以用 plus<> 类完成常规的相加运算:

#include <functional>
...
plus<double> add;		//create a plus<double> object
double y = add(2.2, 3.4);		// using plus<double>::operator()()

它使得将函数对象作为参数很方便:

transform(gr8.begin(), gr8.end(), m8.begin(), out, plus<double> () );

这里,代码没有创建命名的对象,而是用 plus<double> 构造函数构造了一个函数符,以完成相加运算(括号表示调用默认的构造函数,传递给 transform() 的是构造出来的函数对象)。

对于所有内置的算术运算符、关系运算符 和 逻辑运算符,STL 都提供了等价的函数符。下表列出了这些函数符的名称。它们可以用于处理 C++ 内置类型或任何用户定义类型(如果重载了相应的运算符)。

运算符和相应的函数符:

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

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

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

上表列出的预定义函数符都是自适应的。实际上 STL 有 5 个相关的概念:自适应生成器(adaptable generator)、自适应一元函数(adaptable unary function)、自适应二元函数(adaptable binary function)、自适应谓词(adaptable predicate)和自适应二元谓词(adaptable binary predicate)。

使函数符成为自适应的原因是,它携带了表示参数类型和返回类型的 typedef 成员。这些成员分别是 result_type、first_argument_type 和 second_argument_type,它们的作用是不言自明的。例如,plus<int> 对象的返回类型被标识为 plus<int>::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) f1;

这样,使用单个参数调用 f1(x) 时,返回值与将 val 作为第一参数、x 作为第二参数调用f2()返回的值相同。即f1(x) 等价于 f2(val,x),只是前者是一元函数,而不是二元函数。f2() 函数被适配。同样,仅当 f2() 是一个自适应函数时,这才能实现。

看上去优点麻烦。然而,STL 提供了函数 bind1st(),以简化 binder1st 类的使用。可以问其提供用于构建 binder1st 对象的函数名称和值,它将返回一个这种类型的对象。例如,要将二元函数 multiplies() 转换为将参数乘以 2.5 的一元函数,则可以这样做:

bind1st(multiplies<double>(), 2.5)

因此,将 gr8 中的每个元素与 2.5 相乘,并显示结果的代码如下:

transform(gr8.begin(), gr8.end(), out, bind1st(multiplies<double(), 2.5));

binder2nd 类与此类似,只是将常数赋给第二个参数,而不是第一个参数。它有一个名为 bind2nd 的助手函数,该函数的工作方式类似于 bind1st。

下面的程序将一些最近的示例合并成了一个小程序。

// funadap.cpp -- using function adapters
#include <ios>
#include<iostream>
#include<vector>
#include<iterator>
#include<algorithm>
#include<functional>

void Show(double);
const int LIM = 6;

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> gr8(arr1, arr1+LIM);
    vector<double> m8(arr2, arr2+LIM);

    cout.setf(ios_base::fixed);
    cout.precision(1);
    cout << "gr8:\t";
    for_each(gr8.begin(), gr8.end(), Show);
    cout << endl;
    cout << "m8:\t";
    for_each(m8.begin(), m8.end(), Show);
    cout << endl;

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

    vector<double>prod(LIM);
    transform(gr8.begin(), gr8.end(), prod.begin(), bind1st(multiplies<double>(),2.5));
    cout << endl;

    return 0;
}

void Show(double v){
    std::cout.width(6);
    std::cout << v << ' ';
}

该程序的输出如下:

gr8:      28.0   29.0   30.0   35.0   38.0   59.0
m8:       63.0   65.0   69.0   75.0   80.0   99.0
sum:      91.0   94.0   99.0  110.0  118.0  158.0
prod:     70.0   72.5   75.0   87.5   95.0  147.5

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值