C++ template —— 函数对象和回调(十四)

本篇是本系列博文最后一篇,主要讲解函数对象和回调的相关内容。
函数对象(也称为仿函数)是指:可以使用函数调用语法进行调用的任何对象。在C程序设计语言中,有3种类似于函数调用语法的实体:函数、类似于函数的宏和函数指针。由于函数和宏实际上并不是对象,因此在C语言中,我们只把函数指针看成仿函数。然而在C++中,还存在其他的函数对象:对于class类型,我们可以重载函数调用运算符;还存在函数引用的概念;另外,成员函数和成员函数指针也都有自身的调用语法。本篇在于把仿函数的概念和模板所提供的编译期参数化机制结合起来以提供更加强大的程序设计技术。
仿函数的习惯用法几乎都是使用某种形式的回调,而回调的含义是这样的:对于一个程序库,它的客户端希望该程序库能够调用客户端自定义的某些函数,我们就把这种调用称为回调。
------------------------------------------------------------------------------------------------------------
22.1 直接调用、间接调用和内联调用

在阐述如何使用模板来实现有用的仿函数之前,我们先讨论函数调用的一些属性,也正是这些属性的差异,才真正体现出基于模板的仿函数的优点。

在博文“直接调用、间接调用和内联调用”中充分阐明了这里使用内联的优点:在一个调用系列中,不但能够避免执行这些(查找名称的)机器代码;而且能够让优化器看到函数对传递进来的变量进行了哪些操作。

实际上,我们在后面将会看到,如果我们使用基于模板的回调来生成机器码的话,那么这些机器码将主要涉及到直接调用和内联调用;而如果用传统的回调的话,那么将会导致间接调用。根据博文xxxx的讨论,可以知道使用模板的回调将会大大节省程序的运行时间。

22.2 函数指针与函数引用

考虑函数foo()定义:

extern "C++" void foo() throw()
{
}

该函数的类型为:具有C++链接的函数,不接受参数,不返回值并且不抛出异常。由于历史原因,在C++语言的正式定义中,并没有把异常规范并入函数类型的一部分。然而,将来的标准将会把异常加入函数类型中。实际上,当你自己编写的代码要和某个函数进行匹配时,通常也应该要求异常规范同时也是匹配的。名字链接(通常只存在于C和C++中)是类型系统的一部分,但某些C++编译器将会自动添加这种链接。特别地,这些编译器允许具有C链接的函数指针和具有C++链接的函数指针相互赋值。这同时带来下面的一个事实: 在大多数平台上,C和C++函数的调用规范几乎是一样的,唯一的区别在于:C++将会考虑参数的类型和返回值的类型。

在大多数上下文中,表达式foo能够转型为指向函数foo()的指针。即使foo本身并没有指针的含义,但是就如表达式ia一样,在声明了下面的语句之后:

int ia[10];

ia将隐含地表示一个数组指针(或者是一个指向数组第1个元素的指针)。于是,这种从函数(或者数组)到指针的转型通常也被称为decay。如下:

// functors/funcptr.cpp

#include <iostream>
#include <typeinfo>

void foo()
{
    std::cout << "foo() called" << std::endl;
}

typedef void FooT();        // FooT是一个函数类型,与函数foo()具有相同的类型

int main()
{
    foo();     // 直接调用

    // 输出foo和FooT的类型
    std::cout << "Types of foo: " << typeid(foo).name() << '
';
    std::cout << "Types of FooT: " << typeid(FooT).name() << '
';
    
    FooT* pf = foo;            // 隐式转型(decay)
    pf();            // 通过指针的间接调用
    (*pf)();            // 等价于pf()

    // 打印出pf的类型
    std::cout << "Types of pf : " << typeif(pf).name() << '
'; 

    FooT& rf = foo;            // 没有隐式转型
    rf();            // 通过引用的间接调用

    // 输出rf的类型
    std::cout << "Types of rf : " << typeid(rf).name() << '
';
}
//-----------------------------------------------
输出:
foo() called
Types of foo: void()
Types of FooT: void()
foo() called
foo() called
Types of pf: FooT *    // 输出类型不是void(*)而是FooT*
foo() called
Types of rf: void ()

该例子同时也说明了:作为语言的一个概念,函数引用(或者称为指向函数的引用)是存在的;但是我们通常都是使用函数指针(而且为了避免产生混淆,最后还是继续使用函数指针)。另外,表达式foo实际上是一个左值,因为它可以被绑定到一个non-const类型的引用;然而,我们却不能修改这个左值。

我们另外还发现:在函数调用中,可以使用函数指针的名称(如pf)或者函数引用的名称(如rf)来进行函数调用,就像所有函数名称本身一样。因此,可以认为一个函数指针本身就是一个仿函数——一个在函数调用语法中可以用于代替函数名称的对象。另一方面,由于引用并不是一个对象,所有函数引用并不是仿函数。最后,如果基于我们前面所讨论的直接调用和间接调用来看,那么这些看起来相同的符号却很可能会有很大的性能差距。

22.3 成员函数指针

典型的C++实现(也即编译器)是如何处理成员函数调用的?首先考虑下面的程序:

class B1
{
    private:
        int b1;
    public:
        void mf1();
};
void B1::mf1()
{
    std::cout << "b1 = " << b1 << std::endl;
}
//--------------------------------
class B2
{
    private:
        int b2;
    public:
        void mf2();
};
void B2::mf2()
{
    std::cout << "b2 = " << b2 << std::endl;
}
//--------------------------------
class D : public B1, public B2
{
    private:
        int d;
};

对成员函数mf1或mf2调用语法p->mf_x(),p会是一个指向对象或子对象的指针,以某种隐藏参数的形式传递给mf_x,大多是作为this指针的形式传递。 有了上面这个定义之后,D类型对象不但具有B1类型对象的行为,同时也具有B2类型对象的行为。为了实现D类型对象的这种特性,一个D对象就需要既包含一个B1对象,也包含一个B2对象。在我们今天所指定的几乎所有的32位编译器中,D对象在内存中的组织方式都将会如图22.1所示。也就是说,如果int成员占用4个字节的话,那么成员b1的地址为this的地址,成员b2的地址为this地址再加上4个字节,而成员d的地址为this地址加上8个字节。B1和B2最大的区别在于:B1的子对象(即b1)与D的子对象共享起始地址(即this地址),而B2的子对象(即b2)则没有。

现在,考虑使用成员函数指针进行函数调用:

void call_memfun (D obj, void(D::*pmf) () )
{
    (obj.*pmf) ();
}

int main()
{
    D obj;
    call_memfun(obj, &D::mf1);
    call_memfun(obj, &D::mf2);
}

从上面调用代码我们得出一个结论:对于某些成员函数指针,除了需要指定函数的地址之外,还需要知道基于this指针的地址调整。如果在考虑到虚函数的时候又会有其他的许多不同。编译器通常使用3-值结构:
(1)成员函数的地址,如果是一个虚函数的话,那么该值为NULL;
(2)基于this的地址调整;
(3)一个虚函数索引。

《Inside C++ Object Model》里面对此有相关介绍,你同时会发现成员变量指针实际上并不是一个真正意义上的指针,而是一些基于this指针的偏移量,然后根据this指针和对应的偏移量,才能获取给定的域(即成员变量的值,对于值域而言,在内存中可以表示为一块固有的存储空间)。

对于通过成员函数指针访问成员函数的操作,实际上是一个2元操作,因为它不仅仅需要知道对应的成员函数指针(即下面的pmf),还需要知道包含该成员函数的对象(即下面的obj)。于是,在语言中引入特殊的成员指针取引用运算符.*和->*:

(obj.*pmf)(...)            // 调用位于obj中的、pmf所引用的成员函数
(ptr->*pmf)(...)            // 调用位于ptr所引用对象中的、pmf所引用的成员函数

相对而言,通过指针访问一个普通函数就是一个一元操作:

(*ptr)()

从前面我们知道,上面这个解引用运算符可以省略不写,因为在函数调用运算符中,解引用运算符是隐式存在的。因此,前面的表达式通常可以写出:

ptr()

但是对于函数指针而言,却不存在这种隐式(存在)的形式。

注:对于成员函数名称而言,同样不存在隐式的decay,例如MyType::print不能隐式decay为对应的指针形式(即&MyType::print),其中这个&号是必须写的,并不能省略。然而对于普通函数而言,把f隐式decay为&f是很常见的,也是众所周知的。

22.4 class类型的仿函数

在C++语言中,虽然函数指针直接就是现成的仿函数;然而,在很多情况下,如果使用重载了函数调用运算符的class类型对象的话,可以给我们带来很多好处:譬如灵活性、性能,甚至二者兼备。

22.4.1 class类型仿函数的第1个实例

下面是class类型仿函数的一个简单例子:

// functors/functor1.cpp

#include <iostream>
// 含有返回常值的函数对象的类
class ConstantIntFunctor
{
    private:
        int value;    // “函数调用”所返回的值
    public:
        // 构造函数:初始化返回值
        ConstantIntFunctor (int c) : value(c) {}

        // “函数调用”
        int operator() () const {
            return value;
        }
};

// 使用上面“函数对象”的客户端函数
void client (ConstantIntFunctor const& cif)
{
    std::cout << "calling back functor yields " << cif() << '
' ;
}

int main()
{
    ConstantIntFunctor seven(7);
    ConstantIntFunctor fortytwo(42);
    client(seven);
    client(fortytwo);
}

ConstantIntFunctor是一个class类型,而它的仿函数就是根据该类型创建出来的。也就是说,如果你使用下面语句生成一个对象:

ConstantIntFunctor seven(7);        // 生成一个名叫seven的函数对象

那么表达式:

seven();            // 调用函数对象的operator()

就是调用对象seven的operator(),而不是调用函数seven()。实际上,我们传递函数对象seven和fortytwo给client()的参数cif,(间接地)获得了和传递函数指针完全一样的效果。

该例如同时也说明了:在实际应用中,class类型仿函数的优点所在(与函数指针相比):能够在函数中关联某些状态(也即成员变量),这可能也是class类型仿函数最重要的优点。而对于回调机制而言,这种优点能够带来功能上的提升。因为对于一个函数而言,我们现在能够根据不同的参数(主要指成员变量)来生成不同的函数实例(如前面的seven和fortytwo)。

22.4.2 class类型仿函数的类型

与函数指针相比,class类型仿函数除了具有状态信息之外,还具有其他的特性。实际上,如果一个class类型仿函数并没有包含任何状态的话,那么它的行为完全是由它的类型所决定的。于是,我们可以以模板实参的形式来传递该类型,用于自定义程序库组件的行为。

对于上面的这种实现,一个经典的例子是:以某种顺序对它的元素进行排序的容器类,其中排序规则就是一个模板实参。另外,由于排序规则是容器类型的一部分,所以如果对某个特定容器混合使用多种不同的排序规则(例如在赋值运算符中,两个容器使用不同的排序规则,就不能相互赋值),类型系统通常都会给出错误。
C++标准库中的set为例:

#include <set>
 class Person
 {
     ......
 };
class PersonSortCriterion
{
    public:
        bool operator() (Person const& p1, Person const& p2) const
        {
            // 返回p1是否“小于”p2
            ....
        }
};

void foo()
{
        std::set<Person, std::less<Person> > c0, c1;        // 用operator< (小于号)进行排序
        std::set<Person, std::less<Person> > c2;        // 用operator> (大于号)进行排序
        std::set<Person, PersonSortCriterion> c3;        // 用用户自定义的排序规则进行排序
        ...
        c0 = c1;        // 正确:相同的类型
        c1 = c2;        // 错误:不同的类型
        ...
        if (c1 == c3)       // 错误:不同的类型
        {
            .....
        }        
}

22.5 指定仿函数

在我们前面的例子中,我们只给出了一种选择set类的仿函数的方法。在这一节里,我们将讨论其他的几种方法。

22.5.1 作为模板类型实参的仿函数

传递仿函数的一个方法是让它的类型作为一个模板实参。然而类型本身并不是一个仿函数,因此客户端函数或者客户端类必须创建一个给定类型的仿函数对象。当然,只有class类型仿函数才能这么做,函数指针则不可以;而且函数指针本身也不会指定任何行为。另外,也不存在一种能够传递包含状态的类型的机制(因为类型本身并不包含任何特定的状态,只有对象才可能具有某些特定的状态,所以在此真正要传递的是一个特定的对象)。

下面是函数模板的一个雏形,它接收一个class类型的仿函数作为排序规则:

template <typename FO>
void my_sort(... )
{
    FO cmp;        // 创建函数对象
    ... 
    if (cmp(x, y))        // 使用函数对象来比较2个值
    {
        ....
    }
    ....
}
// 以仿函数为模板实参,来调用函数
my_sort<std::less<... > > (... );

运用上面这个方法,比较代码(如std::less<>)的选择将会是在编译期进行的。并且由于比较操作是内联的,所以一个优化的编译器将能够产生本质上等价于不使用仿函数,而直接编写的代码。

22.5.2 作为函数调用实参的仿函数
另一种传递仿函数的方法是以函数调用实参的形式进行传递。这就允许调用者在运行期构造函数对象(可能使用一个非虚拟的构造函数)
就作用而言,函数调用实参和函数类型参数本质上是类似的,唯一的区别在于:当传递参数的时候,函数调用实参需要拷贝一个仿函数对象。这种拷贝开销通常是很低的,而且实际上如果该仿函数对象没有成员变量的话(而实际情况也经常如此),那么这种拷贝开销也将接近于0。如下:

template <typename F>
void my_sort(... , F cmp)
{
    ...
    if (cmp(x, y))        // 使用函数对象,来比较两个值
    {
        ...
    }
    ...
}
// 以仿函数作为调用实参,调用排序函数
my_sort(... , std::less<... >());

22.5.3 结合函数调用参数和模板类型参数
对于前面两种传递仿函数的方式——即传递函数指针和class类型的仿函数,只要通过定义缺省函数调用实参,是完全可以把这两种方式结合起来的:

template <typename F>
void my_sort(... , F cmp = F() )
{
    ...
    if (cmp(x, y))        // 使用函数对象来比较两个值
    {
        ...
    }
    ...
}
bool my_criterion() (T const& x, T const& y);
// 借助于模板实参传递进来的仿函数,来调用排序函数
my_sort<std::less<... > > (... );

// 借助于值实参(即函数实参)传递进来的仿函数,来定义排序函数
my_sort(... , std::less<... >());
// 借助于值实参(即函数实参)传递进来的仿函数,来定义排序函数
my_sort(... , my_criterion);

22.5.4 作为非类型模板实参的仿函数
我们同样也可以通过非类型模板实参的形式来提供仿函数。然而,class类型的仿函数(更普遍而言,应该称为class类型的对象)将不能作为一个有效的非类型模板实参。如下面的代码就是无效的:

class MyCriterion
{
    public:
        bool operator() (SomeType const&, SomeType const&) const;
};

template<MyCriterion F>        // ERROR:MyCriterion 是一个class类型
void my_sort(... );    

然而,我们可以让一个指向class类型对象的指针或者引用作为非类型实参,这也启发了我们编写出下面的代码:

class MyCriterion
{
    public:
        virtual bool operator() (SomeType const&, SomeType const&) const = 0;
};

class LessThan : public MyCriterion
{
    public:
        virtual bool operator() (SomeType const&, SomeType const&) const;
};

template<MyCriterion& F>        // class类型对象的指针或引用
void sort(... );

LessThan order;
sort<order> (... );        // 错误:要求派生类到基类的转型
sort<(MyCriterion&)order>(... );        // 非类型模板实参所引用的必须是一个简单的名称(不能含有转型)

在上面这个例子中,我们的目的是为了在抽象基类中描述这种排序规则的接口,并且在非类型模板实参中使用该抽象类型。就我们的想法而言,我们是为了能够在派生类(如LessThan)中来特定地实现基类的这种接口(MyCriterion)。遗憾的是,C++并不允许这种实现方法,在C++中,借助于引用或者指针的非类型实参必须能够和参数类型精确匹配,从派生类到基类的转型是不允许的,而进行显式类型转换也会使实参无效,同样也是错误的。

据此我们得出一个结论:class类型的仿函数并不适合以非类型模板实参的形式进行传递。相反,函数指针(或者函数引用)却可以是有效的非类型模板实参。

22.5.5 函数指针的封装
本节主要介绍:把一个合法的函数嵌入一个接收class类型仿函数框架。因此,我们可以定义一个模板,从而可以方便地嵌入这种函数:

// functors/funcwrap.cpp

#include <vector>
#include <iostream>
#include <cstdlib>

// 用于把函数指针封装成函数对象的封装类
template <int (*FP)() >
class FunctionReturningIntWrapper
{
    public:
        int operator() (){
            return FP();
        }
};

// 要进行封装的函数实例
int random_int()
{
    return std::rand();        // 调用标准的C函数
}

// 客户端,它使用由模板参数传递进来的函数对象类型
template <typename FO>
void initialize(std::vector<int>& coll)
{
    FO fo;        // 创建函数对象
    for(std::vector<int>::size_type i=0; i<coll.size(); ++i){
        coll[i] = fo();        // 调用由函数对象表示的函数
    }
}

int main()
{
    // 创建含有10个元素的vector
    std::vector<int> v(10);

    // 用封装函数来(重新)初始化vector的值
    initialize<FunctionReturningIntWrapper<random_int> > (v);

    // 输出vector中元素的值
    for(std::vector<int>::size_type i=0; i<v.size(); ++i){
        std::cout << "coll[" << i << "]:" << v[i] << std::endl;
    }
}

其中位于initialize()内部的表达式:

FunctionReturningIntWrapper<random_int>

封装了函数指针random_int,于是我们可以把

FunctionReturningIntWrapper<random_int>

作为一个模板类型参数传递给initialize函数模板。

注意,我们不能把一个具有C链接的函数指针直接传递给类模板FunctionReturningIntWrapper。例如:

initialize<FunctionReturningIntWrapper<std::rand> > (v);

可能就会是错误的,因为std::rand()是一个来自C标准库的函数(因此也就具有C链接)。然而,我们可以引入一个typedef,从而就可以使一个函数指针类型具有合适的链接:

// 针对具有C链接的函数指针的类型
extern "C" typedef int (*C_int_FP) ();

// 把函数指针封装成函数对象的类
template <C_int_FP FP>
class FunctionReturningIntWrapper
{
    public:
        int operator() (){
            return FP();
        }
};

22.6 内省
在程序设计上下文中,内省指的是一种能够查看自身的能力。如查看仿函数接收多少个参数、返回类型和第n个参数的类型等等。
我们可以开发一个仿函数框架,它要求所参与的仿函数都必须提供一些额外的信息,从而可以实现某种程度上的内省。

22.6.1 分析一个仿函数的类型
在我们的框架中,我们只是处理class类型的仿函数,并且要求框架可以提供以下这些于仿函数相关的属性:
(1)仿函数参数的个数(作为一个成员枚举常量NumParams)。
(2)仿函数每个参数的类型(通过成员typedef Param1T、Param2T、Param3T来表示)。
(3)仿函数的返回类型(通过一个成员typedef ReturnT来表示)。

例如,我们可以这样编写PersonSortCriterion,使之适合我们前面的框架:

class PersonSortCriterion
{
    public:
        enum { NumParams = 2 };
        typedef bool ReturnT;
        typedef Person const& Param1T;
        typedef Person const& Param2T;
        bool operator() (Person const& p1, Person const& p2) const {
            // 返回p1是否“小于”p2
            ....
        }
};

对于没有副作用的仿函数,我们通常把它称为纯仿函数。例如,通常而言,排序规则就必须是纯仿函数,否则的话排序操作的结果将会是毫无意义的。

注:至少从某种意义上而言,一些关于缓存和日志的副作用就是可以忽略不计的,因为它们不会对仿函数的返回值产生影响。

22.6.2 访问参数的类型

仿函数可以具有任意数量的参数。我们期望能够编写一个类型函数,对于一个给定的仿函数类型和一个常识N,可以给出该仿函数第N个参数的类型:

// functors/functorparam1.hpp

#include "ifthenelse.hpp"

template <typename F, int N>
class UsedFunctorParam;

template<typename F, int N>
class FunctorParam
{
    private:
        // 当N值大于仿函数的参数个数时的类型:FunctorParam<F, N>::Type的类型为私有class类型
        // 不使用FunctorParam<F, N>::Type的值为void的原因,是因为void自身会有很多限制,如函数不能接受类型为void的参数
        class Unused
        {    
            private:
                // 这种类型的对象不能被创建
                class Private {}
            public:
                typedef Private Type;
        };
    public:
        typedef typename IfThenElse<F::NumParams>=N,
                                                    UsedFunctorParam<F, N>,
                                                    Unused>::ResultT::Type
                    Type;
};

template <typename F>
class UsedFunctorParam<F, 1>
{
    public:
        typedef typename F::Param1T Type;
};

UsedFunctorParam是我们引入的一个辅助模板,对于每一个特定的N值,都需要对该模板进行局部特化,下面使用宏来实现:

// functors/functorparam2.hpp

#define FunctorParamSpec(N)                                                    
            template<typename F>                                                
            class UsedFunctorParam<F, N>{                                    
                public:                                                                    
                    typedef typename F::Param##N##T Type;            
            }
...
FunctorParamSpec(2);
FunctorParamSpec(3);
...
FunctorParamSpec(20);

#undef FunctorParamSpec

22.6.3 封装函数指针

上面一小节,我们借助于typedef的形式,是仿函数类型能够支持某些内省。然而,由于要实现这些内省的约束,函数指针不再适用于我们的框架。我们可以通过封装函数指针来绕过这种限制。我们可以开发一个小工具,它能够封装最多具有2个参数的函数(封装含有多个参数的函数的原理和做法是一样的)。

接下来给出的解释方案将会涉及到2个组件:类模板FunctionPtr,它的实例就是封装函数指针的仿函数类型;重载函数模板func_ptr,它接收一个函数指针为参数,然后返回一个相应的、适合该框架的仿函数。其中,类模板FunctionPtr将由返回类型和参数类型进行参数化:

template<typename RT, typename P1 = void, typename P2 = void>
class FunctionPtr;

用void值来替换一个参数意味着:该参数实际上并没有提供。因此,我们的模板能够处理仿函数调用实参个数不同的情况。
因为我们需要封装的是函数指针,所以我们需要有一个工具,它能够根据参数的类型,来创建函数指针类型。我们通过下面的局部特化来实现这个目的:

// functors/functionptrt.hpp
// 基本模板,用于处理参数个数最大的情况:
template <typename RT, typename P1 = void,
                                    typename P2 = void,
                                    typename P3 = void>
class FunctionPtrT
{
    public:
        enum { NumParams = 3 };
        typedef RT (*Type)(P1, P2, P3);
};

// 用于处理两个参数的局部特化
template <typename RT, typename P1,
                                    typename P2>
class FunctionPtrT<RT, P1, P2, void>
{
    public:
        enum { NumParams = 2 };
        typedef RT (*Type)(P1, P2);
};

// 用于处理一个参数的局部特化
template<typename RT, typename P1>
class FunctionPtrT<RT, P1, void, void>
{
    public:
        enum { NumParams = 1 };
        typedef RT (*Type)(P1);
};

// 用于处理0个参数的局部特化
template<typename RT>
class FunctionPtrT<RT, void, void, void>
{
    public:
        enum { NumParams = 0 };
        typedef RT (*Type)();
};

你会发现,我们还使用了上面这个(相同的)模板来计算参数的个数。

对于上面这个仿函数类型,它把它的参数传递给所封装的函数指针。然而,传递一个函数调用实参是可能会产生副作用的:如果相应的参数属于class类型(而不是一个指向class类型的引用),那么在传递的过程中,将会调用该class类型的拷贝构造函数。为了避免这个(调用拷贝构造函数)额外的开销,我们需要编写一个类型函数;在一般情况下,该类型函数不会改变实参的类型,而当参数是属于class类型的时候,它会产生一个指向该class类型的const引用。借助于在第15章开发的TypeT模板和熟知的IfThenElse功能模板,我们可以这样准确地实现这个类型函数:

// functors/forwardparam.hpp

#ifndef FORWARD_HPP
#define FORWARD_HPP

#include "ifthenelse.hpp"
#include "typet.hpp"
#include "typeop.hpp"

// 对于class类型,ForwardParamT<T>::Type是一个常引用
// 对于其他的所有类型,ForwardParamT<T>::Type是普通类型
// 对于void类型,ForwardParamT<T>::Type是一个哑类型(Unused)
template<typename T>
class ForwardParamT
{
    public:
        typedef typename IfThenElse<TypeT<T>::IsClassT,
                                                    typename TypeOp<T>::RefConstT,
                                                    typename TypeOp<T>::ArgT
                                                    >::ResultT
                    Type;
};
template<>
class ForwardParamT<void>
{
    private:
        class Unused { };
    public:
        typedef Unused Type;
};

#endif // FORWARD_HPP

我们发现这个模板和前面的RParam模板非常相似,唯一的区别在于:在此我们需要把void类型(我们在前面已经说明,void类型是用于代表那些没有提供参数的类型)映射为一个类型,而且该类型必须是一个有效的参数类型。

现在,我们已经能够定义FunctionPtr模板了。另外,由于我们事先并不知道FunctionPtr究竟会接收多少个参数,所以在下面的代码中,我们针对不同个数的参数(但在此我们最多只是针对3个参数),都重载了函数调用运算符:

// functors/functionptr.hpp

#include "forwardparam.hpp"
#include "functionptrt.hpp"

template<typename RT, typename P1 = void,
                                    typename P2 = void,
                                    typename P3 = void>
class FunctionPtr
{
    private:
        typedef typaname FunctionPtrT<RT, P1, P2, P3>::Type FuncPtr;
        // 封装的指针
        FuncPtr fptr;
    public:
        // 使之适合我们的框架
        enum { NumParams = FunctionPtrT<RT, P1, P2, P3>::NumParams };
        typedef RT ReturnT;
        typedef P1 Param1T;
        typedef P2 Param2T;
        typedef P3 Param3T;

        // 构造函数:
        FunctionPtr(FuncPtr ptr) : fptr(ptr) { }

        // "函数调用":
        RT operator() (){
            return fptr();
        }
    
        RT operator() (typename ForwardParamT<P1>::Type a1) {
            return fptr(a1);
        }

        RT operator() (typename ForwardParamT<P1>::Type a1,
                            typename ForwardParamT<P2>::Type a2) {
            return fptr(a1, a2);
        }

        RT operator() (typename ForwardParamT<P1>::Type a1,
                            typename ForwardParamT<P2>::Type a2,
                            typename ForwardParamT<P3>::Type a3) {
            return fptr(a1, a2, a3);
        }
};

该类模板可以实现所期望的功能,但如果直接使用该模板,将会比较繁琐。为了使之具有更好的易用性,我们可以借助模板的实参演绎机制,实现每个对应的(内联的)函数模板:

// functors/funcptr.hpp

#include "functionptr.hpp"

template <typename RT> inline
FunctionPtr<RT> func_ptr (RT (*fp) () )
{
    return FunctionPtr<RT>(fp);
}

template <typename RT, typename P1> inline
FunctionPtr<RT, P1> func_ptr (RT (*fp) (P1) )
{
    return FunctionPtr<RT, P1>(fp);
}

template <typename RT, typename P1, typename P2> inline
FunctionPtr<RT, P1, P2> func_ptr (RT (*fp) (P1, P2) )
{
    return FunctionPtr<RT, P1, P2>(fp);
}

template <typename RT, typename P1, typename P2, typename P3> inline
FunctionPtr<RT, P1, P2, P3> func_ptr (RT (*fp) (P1, P2, P3) )
{
    return FunctionPtr<RT, P1, P2, P3>(fp);
}

至此,剩余的工作就是编写一个使用这个(高级)模板工具的实例程序了。如下所示:

// functors/functordemo.cpp

#include <iostream>
#include <string>
#include <typeinfo>
#include "funcptr.hpp"

double seven()
{
    return 7.0;
}

std::string more()
{
    return std::string("more");
}

template <typename FunctorT>
void demo(FunctorT func)
{
    std::cout << "Functor returns type "
                << typeid(typename FunctorT::ReturnT).name() << '
'
                << "Functor returns value "
                << func() << '
';
}

int main()
{
    demo(func_ptr(seven));
    demo(func_ptr(more));
}

书中在本章最后两节,介绍了函数对象组合和和值绑定的相关知识点及其实现。函数对象组合通过组合两个或多个仿函数,来实现多个仿函数功能的组合,完成较为复杂的操作;而值绑定通过对一个具有多个参数的仿函数,把其中一个参数绑定为一个特定的值。这两节在C++标准库如STL标准库中都能找到对应的实现例子,这里限于篇幅也不作介绍,有兴趣可以参阅书籍《C++ Template》或《STL源码剖析》。

原文地址:https://www.cnblogs.com/yyxt/p/5230432.html