函数对象

函数对象是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 bool std::greater;的模板类。这个模板类需要两个参数来执行operator()。所以我们可以使用这个模板类来重写greater_1000函数:

bool greater_1000(int n)
{
    std::greater gt;
    return gt(n, 1000);
}

进一步,STL提供了std::bind的函数配接器,这个函数配接器可以根据参数返回一个新的函数对象(与迭代器配接器类似)。下面我们使用bind来看看能不能帮助我们减少对greater_1000的依赖。

bool greater_1000(int n)
{
    std::greater gt;
    return (std::bind(gt, n, 1000)) (n);
}

更进一步,我们已经不再需要gt这个临时变量了。

bool greater_1000(int n)
{
    return (std::bind(std::greater(), n, 1000)) (n);
}

此时,原来find_if需要的判断式已经从greater_1000,变成了我们简化后的表达式:std::bind(std::greater(), n, 1000)。这里的问题是我们还需要参数n,所以还不能直接替代。考虑STL提供的占位符功能以后我们就可以将表达式简化为:std::bind(std::greater(), std::placeholders::_1, 1000)。则find_if就可以改写为:

std::find_if(v.begin(), v.end(), std::bind(std::greater(), std::placeholders::_1, 1000));

此时,我们已经完全摆脱了对于greater_1000的依赖,同时如果我们想找到vector中第一个大于x的元素也可以通过:

std::find_if(v.begin(), v.end(), std::bind(std::greater(), 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对象存在的问题:

  1. 只能组合函数,不能组合函数对象。这是因为成员变量被定义为函数指针导致的。
  2. 只能组合两个函数。
    首先我们先处理第一个问题。
    通过引入模板,我们可以将成员变量修改为泛型的方式:
template 
class compose {
    compose(F f0, G g0):f(f0), g(g0) {}
    int operator() (int n) {
        return f(g(n));
    }
private:
    F f;
    G g;
};

上面的模板定义是有缺陷的:

  1. 我们的operator()的定义指明了F必须返回一个int类型且接收一个G(int)的返回类型。
  2. G必须接受一个int类型的参数。
    所以进一步泛型化,我们需要另外两个泛型表明operator()的参数类型和返回类型。
template 
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 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 composition {
public:
    // 这里先忽略...
    Y operator() (X x) const;
   // 这里也先忽略...
};

下面我们需要考虑的就是构造函数应该怎么写?
构造函数应该是有两个参数的f, g,至于f, g的类型可以是函数指针,也可以是函数对象,事实上应该支持存在的所有组合方式,所以构造函数应该是一个模板。

template 
class composition {
public:
    template composition(F, G);
    Y operator() (X x) const;
   // 这里也先忽略...
};

但是这样做存在一个问题:F,G不属于composition的类型。因为他们不在compose类的模板参数中。如果我们添加上F,G,则又回到了原来的问题。如果我们让composition对象保存一个compose对象就可以解决我们的问题,但是同样需要我们将F,G放在composition的模板参数中。

一种包罗万象的类型

看到这标题我们应该就能想起代理类这个特殊的句柄了。是的,我们将通过代理类来解决上面的问题。
假设compose是一个从compose_base继承出来的,那么composition就可以持有compose_base的指针,同时也没有必要在在模板参数列表增加F,G。同时应用代理类的思路,我们的composition对象将持有一类对象,通过动态绑定的方式调用他们的operator()
首先让我们先来实现compose_base:

template 
class compose_base {
public:
    virtual Y operator()(X) const = 0;
    virtual ~compose_base() {} // 我们需要将要被继承的类的析构函数定义为虚析构函数
};

然后我们再来看看composition类,我们应该允许composition类进行复制,如果进行复制的话,我们将面临在不知道具体类型的情况下复制compose_base对象的问题。所以还需要添加一个纯虚函数:clone

template 
class compose_base {
public:
    virtual Y operator()(X) const = 0;
    virtual compose_base* clone() const = 0;
    virtual ~compose_base() {} 
};

下面我们重写compose类:

template 
class compose : public compoose_base {
public:
    compose(F f0, G g0): f(f0), g(g0) {}
    Y operator()(X x) const {
        return f(g(x));
    }
    compose_base* clone() const {
        return (new compose(*this)); // 使用默认的拷贝构造函数
    }
private:
    F f;
    G g;
};

至此,我们终于可以完整的声明composition类了:

template 
class composition {
public:
    template composition(F, G);
    composition(const composition& other);
    composition& operator=(const composition& other);
    ~composition(); // 因为持有了资源,需要释放
    Y operator() (X);
private:
    compose_base* comp;
}

然后,让我们来动手实现它。
首先看构造函数:构造函数应该初始化comp成员,应该使用类compose的对象来初始化该成员。


template 
    template 
        composition::composition(F f0, G g0) :
             comp(new compose(f0, g0)) {}

我们使用一个compose类型的指针初始化compose_base*类型的成员comp。因为compose继承自compose_base所以这个是正确的。
析构函数只是需要简单的释放资源就行了:

template 
composition::~composition() {
    delete comp;
}

因为compose_base存在一个clone的方法,所以拷贝构造和赋值运算符也容易实现:

template 
composition::composition(const composition& other) : comp(other.clone()) {} // 这里调用纯虚函数clone复制实际的对象。

template 
composition& composition::operator=(const composition& other) {
    if (this != &other) { // 注意判断是不是自己赋值给自己
        delete comp;
        comp = other.clone();
    }
    return *this;
}

最后完成operator()的实现:

template 
Y composition::operator() (X x) {
    return (*comp)(x); // 等价于 comp->operator()(x);这里会触发动态绑定。
}

现在我们再来看看使用情况怎么样

extern int f(int);
extern int g(int);
extern int h(int);

int main()
{
    composition fg(f, g);
    fg(42); // f(g(42));
    composition fgh(fg, h);
    fgh(42); // f(g(h(42)));
}

总结

  1. 我们首先通过使用函数对象绕过了c++函数嵌套的限制。
  2. 然后我们仔细的研究了函数对象是如何绕过这个限制的。通过采用代理类的方式可以让我们创建一个包罗万象的(函数指针和函数对象各种组合)的模板类型。
  3. 一旦我们理解了这些概念,我们就可以使用更加简洁的方式使用这个概念。

我们已经定义了更为通用的函数对象模板,但是如果每次要使用STL提供的一种算法的时候都实现一个函数对象是很麻烦的事情。下一篇我们将看看STL为我们提供的函数对象配接器(自动转换为另外一个函数对象的概念)。

你可能感兴趣的:(函数对象)