C++ template —— 函數對象和回調(十四)

本篇是本系列博文最後一篇,主要講解函數對象和回調的相關內容。
函數對象(也稱爲仿函數)是指:可使用函數調用語法進行調用的任何對象。在C程序設計語言中,有3種相似於函數調用語法的實體:函數、相似於函數的宏和函數指針。因爲函數和宏實際上並非對象,所以在C語言中,咱們只把函數指針當作仿函數。然而在C++中,還存在其餘的函數對象:對於class類型,咱們能夠重載函數調用運算符;還存在函數引用的概念;另外,成員函數和成員函數指針也都有自身的調用語法。本篇在於把仿函數的概念和模板所提供的編譯期參數化機制結合起來以提供更增強大的程序設計技術。
仿函數的習慣用法幾乎都是使用某種形式的回調,而回調的含義是這樣的:對於一個程序庫,它的客戶端但願該程序庫可以調用客戶端自定義的某些函數,咱們就把這種調用稱爲回調。
------------------------------------------------------------------------------------------------------------
22.1 直接調用、間接調用和內聯調用ios

在闡述如何使用模板來實現有用的仿函數以前,咱們先討論函數調用的一些屬性,也正是這些屬性的差別,才真正體現出基於模板的仿函數的優勢。數組

在博文「直接調用、間接調用和內聯調用」中充分闡明瞭這裏使用內聯的優勢:在一個調用系列中,不但可以避免執行這些(查找名稱的)機器代碼;並且可以讓優化器看到函數對傳遞進來的變量進行了哪些操做。緩存

實際上,咱們在後面將會看到,若是咱們使用基於模板的回調來生成機器碼的話,那麼這些機器碼將主要涉及到直接調用和內聯調用;而若是用傳統的回調的話,那麼將會致使間接調用。根據博文xxxx的討論,能夠知道使用模板的回調將會大大節省程序的運行時間。app

22.2 函數指針與函數引用 框架

考慮函數foo()定義:less

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

該函數的類型爲:具備C++連接的函數,不接受參數,不返回值而且不拋出異常。因爲歷史緣由,在C++語言的正式定義中,並無把異常規範併入函數類型的一部分。然而,未來的標準將會把異常加入函數類型中。實際上,當你本身編寫的代碼要和某個函數進行匹配時,一般也應該要求異常規範同時也是匹配的。名字連接(一般只存在於C和C++中)是類型系統的一部分,但某些C++編譯器將會自動添加這種連接。特別地,這些編譯器容許具備C連接的函數指針和具備C++連接的函數指針相互賦值。這同時帶來下面的一個事實: 在大多數平臺上,C和C++函數的調用規範幾乎是同樣的,惟一的區別在於:C++將會考慮參數的類型和返回值的類型。dom

在大多數上下文中,表達式foo可以轉型爲指向函數foo()的指針。即便foo自己並無指針的含義,可是就如表達式ia同樣,在聲明瞭下面的語句以後:ide

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() << '\n';
    std::cout << "Types of FooT: " << typeid(FooT).name() << '\n';
    
    FooT* pf = foo;            // 隱式轉型(decay)
    pf();            // 經過指針的間接調用
    (*pf)();            // 等價於pf()

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

    FooT& rf = foo;            // 沒有隱式轉型
    rf();            // 經過引用的間接調用

    // 輸出rf的類型
    std::cout << "Types of rf : " << typeid(rf).name() << '\n';
}
//-----------------------------------------------
輸出:
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() << '\n' ;
}

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() << '\n'
                << "Functor returns value "
                << func() << '\n';
}

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

書中在本章最後兩節,介紹了函數對象組合和和值綁定的相關知識點及其實現。函數對象組合經過組合兩個或多個仿函數,來實現多個仿函數功能的組合,完成較爲複雜的操做;而值綁定經過對一個具備多個參數的仿函數,把其中一個參數綁定爲一個特定的值。這兩節在C++標準庫如STL標準庫中都能找到對應的實現例子,這裏限於篇幅也不做介紹,有興趣能夠參閱書籍《C++ Template》或《STL源碼剖析》。

相關文章
相關標籤/搜索