函数对象
是STL库提供的除了迭代器,迭代器配接器以外的另外一种概念。简单来说:函数对象提供了一种方法,将要调用的函数与准备传递给这个函数的隐藏参数捆绑在一起。即:该对象实现了operator()
的同时还提供了部分执行时的上下文环境。
下面我们通过例子来详细看下函数对象。
例子
STL中有一个find_if
的算法实现,他的参数包括:一组表示范围的迭代器,一个用于生成bool
类型值的判断式
。
例如我们需要在一个vector中找到第一个大于1000的元素:
bool greater_1000(int n)
{
return n > 1000;
}
std::find_if(v.begin(), v.end(), greater_1000);
这样的写法是没有问题的,但是每次使用find_if
查找超过1000的元素的时候都要声明这个greater_1000(int)
的函数是很不方便的,而且这个函数可能只需要运行一次就在也不需要了,但是我们却必须把它定义为函数。下面我们将逐步减少对于greater_1000(int)
的依赖。
通过了解STL提供的其他模板类,我们发现有一个名为:template<class T> bool std::greater;
的模板类。这个模板类需要两个参数来执行operator()
。所以我们可以使用这个模板类来重写greater_1000
函数:
bool greater_1000(int n)
{
std::greater<int> gt;
return gt(n, 1000);
}
进一步,STL提供了std::bind
的函数配接器,这个函数配接器可以根据参数返回一个新的函数对象(与迭代器配接器类似)。下面我们使用bind
来看看能不能帮助我们减少对greater_1000
的依赖。
bool greater_1000(int n)
{
std::greater<int> gt;
return (std::bind(gt, n, 1000)) (n);
}
更进一步,我们已经不再需要gt
这个临时变量了。
bool greater_1000(int n)
{
return (std::bind(std::greater<int>(), n, 1000)) (n);
}
此时,原来find_if
需要的判断式已经从greater_1000
,变成了我们简化后的表达式:std::bind(std::greater<int>(), n, 1000)
。这里的问题是我们还需要参数n
,所以还不能直接替代。考虑STL提供的占位符功能以后我们就可以将表达式简化为:std::bind(std::greater<int>(), std::placeholders::_1, 1000)
。则find_if就可以改写为:
std::find_if(v.begin(), v.end(), std::bind(std::greater<int>(), std::placeholders::_1, 1000));
此时,我们已经完全摆脱了对于greater_1000
的依赖,同时如果我们想找到vector中第一个大于x的元素也可以通过:
std::find_if(v.begin(), v.end(), std::bind(std::greater<int>(), std::placeholders::_1, x));
将x作为参数传给bind来实现。如果不使用函数对象配接器呢?我们几乎是没有办法实现这种通用的功能的。
例如我们不能定义下面这个greater_x(int)
函数:
bool greater_x(int n) {
return n > x;
}
x这个参数不能放在greater_x
的参数列表中,因为find_if
的参数要求判断式只能有一个参数。那么我们只能将x定义为全局变量了,这样使用会更加增加我们的负担。
当然,根据c++11的标准我们在使用find_if时可以采用lambda的方式:
std::find_if(v.begin(), v.end(), [x](int n)-> bool {return n > x;} );
下面我们将看看C++是如何支持的函数对象的。因为我们都知道c++并不是天生支持将函数当作值使用的。c++只能使用函数指针。那么我们先看下函数指针吧
函数指针
同样,我们首先看一个例子。
void apply(void f(int), int*p, int n)
{
for (int i =0; i < n; ++i) {
f(p[i]);
}
}
这个例子具有一定的迷惑性,看起来像是我们传入了一个函数f
处理了数组p
的每一个元素。但是我们要明确这里的f
其实是一个函数指针。上面的写法与下面的写法等同:
void apply(void (*fp)(int), int*p, int n)
{
for (int i =0; i < n; ++i) {
(*fp)(p[i]);
}
}
C++与C一样,对函数指针的调用等同于调用指针所指向的函数的调用。那么函数和函数指针有什么重大的差异吗?使用函数指针也可以让我们假装把函数当作参数使用,把函数指针当作返回值。为啥一定要引入一个奇特的函数对象的概念呢?
函数与函数指针的差异跟普通指针与普通类型一样,即:我们不可能通过操作指针创建一个这样的对象。也就是说:我们有函数指针,但不能通过函数指针创建一个新的函数(这里一个需要注意的是:如果使用了动态链接库可以在运行时找到函数指针的函数体,但是不能说在运行时创建了一个新的函数)。
如果我们需要将两个函数组合生成第三个函数,那么这样的c++函数应该怎样定义?
我们可以根据我们的想象先给出一个期望:
extern int f(int);
extern int g(int);
int (*compose(int f(int), int g(int))) (int x) {
int result(int n) { return f(g(n)); }
return result;
}
compose
是一个接收两个int (*) (int)
类型参数,返回值为int (*) (int)
类型的函数。很不幸,C++不支持在函数中定义一个函数。如果我们假设有些C++实现了这个嵌套函数的feature,会发生什么呢?
int (*compose(int f(int), int g(int))) (int x) {
int (*fp)(int) = f;
int (*gp)(int) = g;
int result(int n) { return fp(gp(n)); }
return result;
}
此时,一旦compose函数执行完成,返回了一个int(*)(int)
类型的result,当result执行的时候,局部变量fp,gp
已经被compose函数销毁了。上面的第一个例子也同样存在这个问题,形参在compose返回时也一样会被销毁。
所以当我们尝试使用函数指针的方式创建一个新的函数的时候,我们遇到了C++语言的限制。我们需要一些其他的内存,用来保证在result执行的时候可以正常的访问f和g。下面我们看看函数对象是如何实现这个功能的。
函数对象
首先我们来看下什么是函数对象:函数对象是一种类类型,该类类型包含一个operator()
的成员函数。
例如:
class F {
public:
int operator() (int);
};
int main {
F f;
int n = f(42);
}
其中f(42)
等价于f,operator()(42);
。
有了这个概念我们来看看能否实现我们的期望。
class compose {
public:
compose(int (*f)(int), int (*g)()):fp(f),gp(g) {}
int operator() (int n) {
return (*fp)((*gp)(n)); // 进行函数组合
}
private:
int (*fp)(int); // 使用成员变量保存函数指针
int (*gp)(int);
};
我们定义了函数对象类compose
,下面我们看看使用效果如何:
extern int f(int);
extern int g(int);
int main()
{
compose comp(f, g);
comp(42); // 这里等价于f(g(42));
}
通过引入函数对象类,我们完美的解决了函数嵌套的语言障碍。
下面让我们根据这个思路看下如何进行更加通用化的改造。
函数对象模板
我们先看下compose
对象存在的问题:
- 只能组合函数,不能组合函数对象。这是因为成员变量被定义为函数指针导致的。
- 只能组合两个函数。
首先我们先处理第一个问题。
通过引入模板,我们可以将成员变量修改为泛型的方式:
template <class F, class G>
class compose {
compose(F f0, G g0):f(f0), g(g0) {}
int operator() (int n) {
return f(g(n));
}
private:
F f;
G g;
};
上面的模板定义是有缺陷的:
- 我们的
operator()
的定义指明了F必须返回一个int类型且接收一个G(int)的返回类型。 - G必须接受一个int类型的参数。
所以进一步泛型化,我们需要另外两个泛型表明operator()
的参数类型和返回类型。
template <class F, class G, class X, class Y>
class compose {
compose(F f0, G g0):f(f0), g(g0) {}
Y operator() (X n) {
return f(g(n));
}
private:
F f;
G g;
};
我们来看看使用效果:
extern int f(int);
extern int g(int);
int main()
{
compose<int (*)(int), int (*)(int), int , int> comp(f, g);
comp(42); // 这里等价于f(g(42));
}
在使用过程中连着写两次int (*)(int)
是一件不好的事情,如果我们考虑组合f(g(f(x)))
的话声明这个类型如下:
compose< compose< int (*)(int), int (*)(int), int , int >, int (*)(int), int, int > fgf(fg, f);
这简直是一场灾难。
能不能简化这个问题呢?
隐藏中间类型
我们期望只需要指定X,Y
就行了,F,G
让编译器帮我们通过fg(f, g)
进行推导。
我们先写出这个模板的声明:
template <class X, class Y>
class composition {
public:
// 这里先忽略...
Y operator() (X x) const;
// 这里也先忽略...
};
下面我们需要考虑的就是构造函数应该怎么写?
构造函数应该是有两个参数的f, g
,至于f, g
的类型可以是函数指针,也可以是函数对象,事实上应该支持存在的所有组合方式,所以构造函数应该是一个模板。
template <class X, class Y>
class composition {
public:
template<class F, class G> composition(F, G);
Y operator() (X x) const;
// 这里也先忽略...
};
但是这样做存在一个问题:F,G不属于composition的类型。因为他们不在compose类的模板参数中。如果我们添加上F,G,则又回到了原来的问题。如果我们让composition对象保存一个compose<F,G,X,Y>对象就可以解决我们的问题,但是同样需要我们将F,G放在composition的模板参数中。
一种包罗万象的类型
看到这标题我们应该就能想起代理类这个特殊的句柄了。是的,我们将通过代理类来解决上面的问题。
假设compose<F,G,X,Y>是一个从compose_base<X,Y>继承出来的,那么composition就可以持有compose_base的指针,同时也没有必要在在模板参数列表增加F,G。同时应用代理类的思路,我们的composition对象将持有一类对象,通过动态绑定的方式调用他们的operator()
。
首先让我们先来实现compose_base:
template <class X, class Y>
class compose_base {
public:
virtual Y operator()(X) const = 0;
virtual ~compose_base() {} // 我们需要将要被继承的类的析构函数定义为虚析构函数
};
然后我们再来看看composition类,我们应该允许composition类进行复制,如果进行复制的话,我们将面临在不知道具体类型的情况下复制compose_base对象的问题。所以还需要添加一个纯虚函数:clone
template <class X, class Y>
class compose_base {
public:
virtual Y operator()(X) const = 0;
virtual compose_base* clone() const = 0;
virtual ~compose_base() {}
};
下面我们重写compose类:
template <class F, class G, class X, class Y>
class compose : public compoose_base<X, Y> {
public:
compose(F f0, G g0): f(f0), g(g0) {}
Y operator()(X x) const {
return f(g(x));
}
compose_base<X, Y>* clone() const {
return (new compose(*this)); // 使用默认的拷贝构造函数
}
private:
F f;
G g;
};
至此,我们终于可以完整的声明composition类了:
template <class X, class Y>
class composition {
public:
template<class F, class G> composition(F, G);
composition(const composition& other);
composition& operator=(const composition& other);
~composition(); // 因为持有了资源,需要释放
Y operator() (X);
private:
compose_base<X, Y>* comp;
}
然后,让我们来动手实现它。
首先看构造函数:构造函数应该初始化comp
成员,应该使用类compose
的对象来初始化该成员。
template <class X, class Y>
template <class F, class G>
composition<X,Y>::composition(F f0, G g0) :
comp(new compose<F, G, X, Y>(f0, g0)) {}
我们使用一个compose<F,G,X,Y>
类型的指针初始化compose_base<X,Y>*
类型的成员comp
。因为compose<F,G,X,Y>
继承自compose_base<X,Y>
所以这个是正确的。
析构函数只是需要简单的释放资源就行了:
template <class X, class Y>
composition<X, Y>::~composition() {
delete comp;
}
因为compose_base<X, Y>
存在一个clone
的方法,所以拷贝构造和赋值运算符也容易实现:
template <class X, class Y>
composition<X, Y>::composition(const composition<X, Y>& other) : comp(other.clone()) {} // 这里调用纯虚函数clone复制实际的对象。
template <class X, class Y>
composition<X, Y>& composition<X, Y>::operator=(const composition<X, Y>& other) {
if (this != &other) { // 注意判断是不是自己赋值给自己
delete comp;
comp = other.clone();
}
return *this;
}
最后完成operator()
的实现:
template <class X, class Y>
Y composition<X, Y>::operator() (X x) {
return (*comp)(x); // 等价于 comp->operator()(x);这里会触发动态绑定。
}
现在我们再来看看使用情况怎么样
extern int f(int);
extern int g(int);
extern int h(int);
int main()
{
composition<int ,int> fg(f, g);
fg(42); // f(g(42));
composition<int, int> fgh(fg, h);
fgh(42); // f(g(h(42)));
}
总结
- 我们首先通过使用函数对象绕过了c++函数嵌套的限制。
- 然后我们仔细的研究了函数对象是如何绕过这个限制的。通过采用代理类的方式可以让我们创建一个包罗万象的(函数指针和函数对象各种组合)的模板类型。
- 一旦我们理解了这些概念,我们就可以使用更加简洁的方式使用这个概念。
我们已经定义了更为通用的函数对象模板,但是如果每次要使用STL提供的一种算法的时候都实现一个函数对象是很麻烦的事情。下一篇我们将看看STL为我们提供的函数对象配接器(自动转换为另外一个函数对象的概念)。