简介(Introduction)
简介(Introduction)
每个使用c++标准模板库(STL)的人都会碰到一些叫做函数对象的奇怪的东西。尽管他们与其他的普通的c++对象一样,但是他们通常与标准模板库(STL)相关,而且实际上这些函数对象被广泛的使用了。更准确的说,函数对象大多数情况下是与STL中的算法一起使用的。
像STL的其他部分一样,函数对象的概念是相当复杂的,因此尽管我们尝试用最简单的方式来解释这件事情,但是,我还是希望你知道一些基本的编程知识和一些STL的知识,特别是STL中的算法。
函数对象基础(Basics of Function Objects)
如果你将函数对象(function objects)分成两个单词:函数(function)和对象(object),显然你已经很了解他们了。如果你已经是一个程序员(当然,这已经是我们假设的事情了),你知道这两者在软件工程中的意思。
一个函数是在c++中完成一件工作(job)最常用的方式;换句话说,函数定义了一系列操作的执行流。它通常有一个定义的入口和一个定义的出口。关于函数应该有一个还是几个出口有很多争论。然而,这对本文没有影响。
一个对象是构建的一个具体的数据类型,它在内存中带有一个确定大小的地址空间。对象(objects)这个单词可以和实例(instance)互换。
一个函数对象使用c++的面向对象特性,并且继承了函数的特点。因此,你可以把函数对象看做一个巧妙的(smart),比普通函数拥有更多优点的函数。几个突出的优点如下:
①:函数对象可以拥有像成员数据一样的成员函数。因此,你可以携带状态(state)甚至可以在相同的时间拥有不同的状态。
②:函数对象在使用之前可以被初始化
③:函数对象比传统的函数执行效率更高
④:不同于通常的函数如果想有不同的类型就需要不同的签名(signature),函数对象甚至可以在有用不同的类型下使用相同的签名(signature),并且可以保证类型安全。
创建函数对象(Creation of Function Objects)
函数对象因为上面提到的四个优点而常常被看做是一种微妙的函数。这也就意味着函数和函数对象是非常类似的。因此,任何像函数那样的对象都可被看做是函数对象。
那么,函数对象神奇的地方在哪里?考虑一下下面的函数调用:
void func(int);
int main()
{
func(3);
return 0;
}
通过观察这个非常简单的例子,你可以将一个函数调用分成三个部分:
①:通过名字调用函数
②:使用括号
③:选择性的提供参数
因此,为了创建一个函数对象是它表现的像一个普通的函数,我们需要提供一种方式来调用这个对象,这种方式不是别的,也是通过对象的名字、括号和可选的参数。解决的方法就是提供所谓的函数调用(function call)操作符的重载。这个操作符就是一对圆括号。
class foo
{
public:
// ...
return_type operator operator_name (argument_list);
// ...
};
因此,为了创建函数对象,你需要简单的提供一个函数调用操作符的
class function_object
{
public:
// ...
int operator()(int i) { return i; }
// ...
};
这是最基础的,现在你就可以像调用函数那样调用函数对象了
#include <iostream>
int main()
{
function_object fo;
// calls operator '()' of class 'function_object'
std::cout << fo(5) << std::endl;
// calls operator '()' of class 'function_object'
std::cout << fo.operator()(2) << std::endl;
return 0;
}
函数对象的分类(Classification of Function Objects)
函数对象的分类有很多准则,但是最重要的一个准则是:是否函数对象带有状态(state)。根据此准则,函数对象可以分为三类。
无状态(stateless)
无状态函数对象是最接近函数的的函数对象。这种函数对象没有数据成员,或者,有数据成员,但是该数据成员对函数调用操作符没有影响(也就是说,不被函数调用操作符使用,包括直接、间接)
#include <iostream>
class stateless_function_object
{
public:
int operator()(int i) const { return i * i; }
};
int main()
{
stateless_function_object fo;
std::cout << fo(2) << std::endl; // will output 4
return 0;
}
Note:如果stateless_function_object拥有任何数据成员,只能说这个函数对象类是一个非常糟糕的设计:一个类的一个职责就是知道自己的目的是要做什么。
有状态,但是数据成员不可变(state-invariable)
这种类型的函数对象拥有内部状态,但是函数调用操作符(call operator)被声明成const型。这也就意味着这个函数调用操作符可以使用状态,但是不可以改变他。
#include <iostream>
class invariant_function_object
{
public:
invariant_function_object(int state): state_(state) {}
int operator()(int i) const { return i * state_; }
private:
int state_;
};
int main()
{
invariant_function_object fo(3);
std::cout << fo(2) << std::endl; // will output 6
return 0;
}
在main函数中的第一行创建了一个函数对象,并且调用了构造函数,设置了它的内部状态为3,第二行调用了函数对象的函数调用操作符(call operator)
就像你所看到的,这个类里面的函数调用操作符使用了内部状态,但是没有改变他。任何导致内部状态发生改变的操作都会导致编译器报错,因为函数调用操作符(call operator)不允许修改内部的数据成员。
有状态,且内部状态可变(state-variable)
这种类型的函数对象不仅拥有状态,而且在每次操作中都可以改变状态。
#include <iostream>
#include <vector>
#include <algorithm>
class stateful_function_object
{
public:
stateful_function_object(): state_(0) {}
int operator()(int i) { return state_ += i; }
int get() const { return state_; }
private:
int state_;
};
int main()
{
std::vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
vec.push_back(5);
stateful_function_object fo;
fo = std::for_each(vec.begin(), vec.end(), fo);
// will output 15 (1 + 2 + 3 + 4 + 5)
std::cout << fo.get() << std::endl;
return 0;
}
让我们仔细看一下,上面的代码究竟做了什么:
①:这个函数对象的基本功能是计算这个由1到5做成的集合的和。这个和操作是通过传递1到5这个集合(collection)给STL的算法for_each来实现的。这个算法将传递集合中的从开始到结束的所有值到函数对象或者一个函数。
②:为了能够得到结果(在这个例子中,是所有元素的和),算法for_each返回了。。未完待续