本篇是本系列博文最后一篇,主要講解函數對象和回調的相關內容。
函數對象(也稱為仿函數)是指:可以使用函數調用語法進行調用的任何對象。在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() << '\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源碼剖析》。
