函数对象

函数对象是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对象存在的问题:

  1. 只能组合函数,不能组合函数对象。这是因为成员变量被定义为函数指针导致的。
  2. 只能组合两个函数。
    首先我们先处理第一个问题。
    通过引入模板,我们可以将成员变量修改为泛型的方式:
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;
};

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

  1. 我们的operator()的定义指明了F必须返回一个int类型且接收一个G(int)的返回类型。
  2. 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)));
}

总结

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,110评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,443评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,474评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,881评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,902评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,698评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,418评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,332评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,796评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,968评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,110评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,792评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,455评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,003评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,130评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,348评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,047评论 2 355

推荐阅读更多精彩内容