作者:唐風(fēng)
原載:www.cnblogs.com/liyiwen
關(guān)于類(lèi)型擦除,在網(wǎng)上搜出來(lái)的中文資料比較少,而且一提到類(lèi)型擦除,檢索結(jié)果里就跑出很多 Java 和 C# 相關(guān)的文章來(lái)(它們實(shí)現(xiàn)“泛型”的方式)。所以,這一篇我打算寫(xiě)得稍微詳細(xì)一點(diǎn)。 注意,這是一篇讀書(shū)筆記(《C++ template metaprogramming》第9.7小節(jié)和《C++ テンプレートテクニック》第七章),里面的例子都來(lái)自原書(shū)。
在 C++ 中,編譯器在編譯期進(jìn)行的靜態(tài)類(lèi)型檢查是比較嚴(yán)格的,但有時(shí)候我們卻希望能“避過(guò)”這樣的類(lèi)型檢查,以實(shí)現(xiàn)更靈活的功能,同時(shí)又盡量地保持類(lèi)型安全。聽(tīng)起來(lái)很矛盾,而且貌似很難辦到。但其實(shí) C++ 的庫(kù)里已經(jīng)有很多這樣的應(yīng)用了。比如,著名的 boost::function 和 boost::any 。當(dāng)我們定義一個(gè) function<void(int)> fun 對(duì)象,則 fun 即可以存儲(chǔ)函數(shù)指針,又可以存儲(chǔ)函數(shù)對(duì)象,注意,這兩者是不同“類(lèi)型”的,而且函數(shù)對(duì)象可以是無(wú)限種類(lèi)型的,但這些不同類(lèi)型的“東西”都可以存在同一類(lèi)型的對(duì)象 fun 中,對(duì) fun 來(lái)說(shuō),它關(guān)心的只是存儲(chǔ)的“對(duì)象”是不是“可以按某種形式(如void(int))來(lái)調(diào)用”,而不關(guān)心這個(gè)“對(duì)象”是什么樣的類(lèi)型。有了 function 這樣的庫(kù),在使用回調(diào)和保存可調(diào)用“對(duì)象”的時(shí)候,我們就可以寫(xiě)出更簡(jiǎn)單且更好用的代碼來(lái)。再舉一個(gè)例子,boost::any 庫(kù)。any 可以存儲(chǔ)任何類(lèi)型的“對(duì)象”,比如 int ,或是你自己定義的類(lèi) MyCla 的對(duì)象。這樣我們就可以使一個(gè)容器(比如 vector<boost::any> )來(lái)存儲(chǔ)不同類(lèi)型的對(duì)象了。
這些庫(kù)所表現(xiàn)出來(lái)的行為,就是這篇文章中要提到的類(lèi)型擦除,類(lèi)型擦除可以達(dá)到下面兩個(gè)目的:
- 用類(lèi)型 S 的接口代表一系列類(lèi)型 T 的的共性。
- 如果 s 是 S 類(lèi)型的變量,那么,任何 T 類(lèi)型的的對(duì)象都可以賦值給s。
好了,下面我們具體地看看類(lèi)型擦除是怎么回事,在這個(gè)過(guò)程中,我們先以 any 這個(gè)類(lèi)為依托來(lái)解釋?zhuān)ㄒ驗(yàn)樗容^“簡(jiǎn)單”,要解釋的額外的東西比較少)。
any 這個(gè)類(lèi)需要完成的主要任務(wù)是:1. 存儲(chǔ)任何類(lèi)型的變量 2. 可以相互拷貝 3. 可以查詢(xún)所存變量的類(lèi)型信息 4. 可以轉(zhuǎn)化回原來(lái)的類(lèi)型(any_cast<>)
對(duì)于其中,只要說(shuō)明1和2 ,就能把類(lèi)型擦除的做法展示出來(lái)了,所以,我們這里只實(shí)現(xiàn)一個(gè)簡(jiǎn)單的,有1、2、3功能的any類(lèi)(3是為了驗(yàn)證)。
首先,寫(xiě)個(gè)最簡(jiǎn)單的“架子”出來(lái):
class my_any {
?? content_obj;
public:
template <typename T>
my_any(T const& a_right);
};
這里,由于 my_any 的拷貝構(gòu)造函數(shù)使用的是模板函數(shù),因此,我們可以任何類(lèi)型的對(duì)象來(lái)初始化,并把該對(duì)象的復(fù)本保存在 content_obj 這個(gè)數(shù)據(jù)成員中。那么,問(wèn)題是,content_obj 用什么類(lèi)型好呢?
首先我們會(huì)想到,給 class 加個(gè)模板參數(shù) T ,然后……,不用然后了,這樣的話(huà),使用者需要寫(xiě)這樣的代碼:
my_any<someType> x = y;
不同的 y 會(huì)創(chuàng)造出不同類(lèi)型的 x 對(duì)象,完全不符合我們要將不同類(lèi)型對(duì)象賦給同一類(lèi)型對(duì)象的初衷。接著,我們會(huì)想到用 void *(C 式的泛型手法啊……),但這樣的話(huà),我們就會(huì)完全地丟失原對(duì)象的信息,使得后面一些操作(拷貝、還原等)變得很困難,那么,再配合著加入一些變量用于保存原對(duì)象信息?你是說(shuō)用類(lèi)似“反射”的能力?好吧,我只好說(shuō),我以為 C++ 不存在原生的反射能力,以我淺薄的認(rèn)識(shí),我只知道像 MFC 式的侵入式手法……,嗯,此路不通。
這個(gè)困境的原因在于,在C++ 的類(lèi)中,除了類(lèi)模板參數(shù)之外,無(wú)法在不同的成員(函數(shù)、數(shù)據(jù)成員)之間共享類(lèi)型信息。在這個(gè)例子中,content_obj 無(wú)法得知構(gòu)造函數(shù)中的 T 是什么類(lèi)型。所以類(lèi)型無(wú)法確定。
為了妥善保存原對(duì)象復(fù)本,我們定義兩個(gè)輔助類(lèi),先上代碼(來(lái)自 boost::any 的原碼):
class placeholder
{
public: // structors
virtual ~placeholder() {
}
public: // queries
virtual const std::type_info & type() const = 0;
virtual placeholder * clone() const = 0;
};
template<typename ValueType>
class holder : public placeholder
{
public: // structors
holder(const ValueType & value): held(value)
{
}
public: // queries
virtual const std::type_info & type() const {
return typeid(ValueType);
}
virtual placeholder * clone() const {
return new holder(held);
}
public: // representation
ValueType held;
};
首先,定義了一個(gè)基類(lèi) placeholder ,它是一個(gè)非模板的抽象類(lèi),這個(gè)抽象類(lèi)的兩個(gè)接口是用來(lái)抽取對(duì)保存在 my_any 中的各種類(lèi)型對(duì)象的共性的,也就是,我們需要對(duì)被保存在 my_any 中的數(shù)據(jù)進(jìn)行拷貝和類(lèi)型查詢(xún)。
然后用一個(gè)模板類(lèi) holder 類(lèi)繼承 placeholder 類(lèi),這個(gè)(類(lèi))派生類(lèi)實(shí)現(xiàn)了基類(lèi)的虛函數(shù),并保存了相關(guān)的數(shù)據(jù)。注意,派生類(lèi)的數(shù)據(jù)成員的類(lèi)型是 ValueType,也就是完整的原對(duì)象類(lèi)型,由于它是個(gè)模板類(lèi),各個(gè)類(lèi)成員之間可以共享類(lèi)模板參數(shù)的信息,所以,可以方便地用原數(shù)據(jù)類(lèi)型來(lái)進(jìn)行各種操作。
有了這兩個(gè)輔助類(lèi),我們就可以這樣寫(xiě) my_any 了:
class My_any
{
placeholder * content_obj;
public:
template <typename T>
My_any(T const& a_right):content_obj(new T(a_right))
{}
template <typename T>
My_any & operator = (T const& a_right) {
delete content_obj;
content_obj = new T(a_right);
return *this;
}
My_any(My_any const& a_right)
: content_obj(a_right.content_obj ?
a_right.content_obj->clone() : 0)
{
}
std::type_info& type() const {
return content_obj ? content_obj->type() : typeid(void);
}
};
現(xiàn)在 my_any 類(lèi)的 content_obj 的類(lèi)型定義成 placeholder * ,在構(gòu)造函數(shù)(和賦值運(yùn)算符)中,我們使用 holder 類(lèi)來(lái)生成真實(shí)的“備份”,由于 holder 是模板類(lèi),它可以根據(jù)賦值的對(duì)象相應(yīng)地保存要我們需要的信息。這樣,我們就完成了在賦值的時(shí)候的“類(lèi)型擦除”啦。在 my_any 的 public 接口( type() )中,利用 placeholder 的虛函數(shù),我們就可以進(jìn)行子類(lèi)提供的那些操作,而子類(lèi),已經(jīng)完整地保存著我們需要的原對(duì)象的信息。
接著我們看下 boost::function 中的 Type Erasure。相比起 boost::any 來(lái),function 庫(kù)要復(fù)雜得多,因?yàn)檫@里只是想講 boost::function 中的“類(lèi)型擦除”,而不是 boost::function 源碼剖析,所以,我們?nèi)匀槐局?jiǎn)化簡(jiǎn)化再簡(jiǎn)化的目的,只挑著討論一些“必要”的成分。
我們假設(shè) function 不接受任何參數(shù)。為了更好的說(shuō)明,我先帖代碼,再一步一步解釋?zhuān)⒁猓旅媸且黄谆ɑǖ拇a,幾沒(méi)有注釋?zhuān)f(wàn)別開(kāi)罵,請(qǐng)?zhí)^(guò)這段代碼,后面會(huì)有分段的解釋?zhuān)?/font>#include <iostream>
#include <boost/type_traits/is_pointer.hpp>
#include <boost/mpl/if.hpp>
using namespace std;
union any_callable {
void (*fun_prt) (); // 函數(shù)指針
void * fun_obj; // 函數(shù)對(duì)象
};
template<typename Func, typename R>
struct fun_prt_manager {
static R invoke(any_callable a_fp) {
return reinterpret_cast<Func>(a_fp.fun_prt)();
}
static void destroy(any_callable a_fp) {}
};
template<typename Func, typename R>
struct fun_obj_manager {
static R invoke(any_callable a_fo) {
return (*reinterpret_cast<Func*>(a_fo.fun_obj))();
}
static void destroy(any_callable a_fo) {
delete reinterpret_cast<Func*>(a_fo.fun_obj);
}
};
struct funtion_ptr_tag {};
struct funtion_obj_tag {};
template <typename Func>
struct get_function_tag {
typedef typename boost::mpl::if_<
boost::is_pointer<Func>, // 在 VC10 中標(biāo)準(zhǔn)庫(kù)已經(jīng)有它啦
funtion_ptr_tag,
funtion_obj_tag
>::type FunType;
};
template <typename Signature>
class My_function;
template <typename R>
class My_function<R()> {
R (*invoke)(any_callable);
void (*destory)(any_callable);
any_callable fun;
public:
~My_function() {
clear();
}
template <typename Func>
My_function& operator = (Func a_fun) {
typedef typename get_function_tag<Func>::FunType fun_tag;
assign(a_fun, fun_tag());
return *this;
}
R operator () () const {
return invoke(fun);
}
template <typename T>
void assign (T a_funPtr, funtion_ptr_tag) {
clear();
invoke = &fun_prt_manager<T, R>::invoke;
destory = &fun_prt_manager<T, R>::destroy;
fun.fun_prt = reinterpret_cast<void(*)()>(a_funPtr);
}
template <typename T>
void assign (T a_funObj, funtion_obj_tag) {
clear();
invoke = &fun_obj_manager<T, R>::invoke;
destory = &fun_obj_manager<T, R>::destroy;
fun.fun_obj = reinterpret_cast<void*>(new T(a_funObj));
}
private:
void clear() {
if (!destory) {
destory(fun);
destory = 0;
}
}
};
int TestFun() {
return 0;
}
class TestFunObj {
public:
int operator() () const {
return 1;
}
};
int main(int argc, char* argv[])
{
My_function<int ()> fun;
fun = &TestFun;
cout<<fun()<<endl;
fun = TestFunObj();
cout<<fun()<<endl;
}
首先需要考慮的是,數(shù)據(jù)成員放什么?因?yàn)槲覀冃枰鎯?chǔ)函數(shù)指針,也需要存儲(chǔ)函數(shù)對(duì)象,所以,這里定義一個(gè)聯(lián)合體:
union any_callable {
void (*fun_prt) (); // 函數(shù)指針
void * fun_obj; // 函數(shù)對(duì)象
};
用來(lái)存放相應(yīng)的“調(diào)用子”。另外兩個(gè)數(shù)據(jù)成員(函數(shù)指針)是為了使用上的方便,用于存儲(chǔ)分別針對(duì)函數(shù)指針和函數(shù)對(duì)象的相應(yīng)的“操作方法”。對(duì)于函數(shù)指針和函數(shù)對(duì)象這兩者,轉(zhuǎn)型(cast)的動(dòng)作都是不一樣的,所以,我們定義了兩個(gè)輔助類(lèi) fun_prt_manager 和 fun_obj_manager,它們分別定義了針對(duì)函數(shù)指針和函數(shù)對(duì)象進(jìn)行類(lèi)型轉(zhuǎn)換,然后再引發(fā)相應(yīng)的“調(diào)用”和“銷(xiāo)毀”的動(dòng)作。
接下來(lái)是類(lèi)的兩個(gè) assign 函數(shù),它們針對(duì)函數(shù)針指和函數(shù)對(duì)象,分別用不同的方法來(lái)初始化類(lèi)的數(shù)據(jù)成員,你看:
invoke = &fun_prt_manager<T, R>::invoke;
destory = &fun_prt_manager<T, R>::destroy;
fun.fun_prt = reinterpret_cast<void(*)()>(a_funPtr);
當(dāng) My_function 的對(duì)象是用函數(shù)指針賦值時(shí),invoke 被 fun_prt_manager 的 static 來(lái)初始化,這樣,在“調(diào)用”時(shí)就把數(shù)據(jù)成員轉(zhuǎn)成函數(shù)指針。同理,可以知道函數(shù)對(duì)象時(shí)相應(yīng)的做法(這里就不啰嗦了)。
但如何確定在進(jìn)行賦值時(shí),哪一個(gè) assign 被調(diào)用呢?我想,熟悉 STL 的你,看到 funtion_ptr_tag 和 funtion_obj_tag 時(shí)就笑了,是的,這里的 get_function_tag 用了 type_traise 的技法,并且,配合了 boost::mpl 提供的靜態(tài) if_ 簡(jiǎn)化了代碼。這樣,我們就完成了賦值運(yùn)算符的編寫(xiě):
template <typename Func>
My_function& operator = (Func a_fun) {
typedef typename get_function_tag<Func>::FunType fun_tag;
assign(a_fun, fun_tag());
return *this;
}
有了這個(gè)函數(shù),針對(duì)函數(shù)指針和函數(shù)對(duì)象,My_function 的數(shù)據(jù)成員都可以正確的初始化了。
如我們所見(jiàn),在 My_function 中,使用了很多技巧和輔助類(lèi),以使得 My_funtion 可以獲取在內(nèi)部保存下函數(shù)指針或是函數(shù)對(duì)象,并在需要的時(shí)候,調(diào)用它們。函數(shù)指針或是函數(shù)對(duì)象,一旦賦值給 My_funtion,在外部看來(lái),便失去了原來(lái)的“類(lèi)型”信息,而只剩下一個(gè)共性——可以調(diào)用(callable)
這兩個(gè)例子已經(jīng)向你大概展示了 C++ 的“類(lèi)型擦除”,最后,再補(bǔ)充一下我的理解:C++中所說(shuō)的“類(lèi)型擦除”不是有“標(biāo)準(zhǔn)實(shí)現(xiàn)”的一種“技術(shù)”(像 CRTP 或是 Trais 技術(shù)那樣有明顯的實(shí)現(xiàn)“規(guī)律”),而更像是從使用者角度而言的一種“行為模式”。比如對(duì)于一個(gè) boost::function 對(duì)象來(lái)說(shuō),你可以用函數(shù)指針和函數(shù)對(duì)象來(lái)對(duì)它賦值,從使用者的角度看起來(lái),就好像在賦值的過(guò)程中,funtion pointer 和 functor 自身的類(lèi)型信息被抹去了一樣,它們都被“剝離成”成了boost::function 對(duì)象的類(lèi)型,只保留了“可以調(diào)用”這么一個(gè)共性,而 boost::any ,則只保留各種類(lèi)型的“type查詢(xún)”和“復(fù)制”能力這兩個(gè)“共性”,其它類(lèi)型信息一概抹掉。這種“類(lèi)型擦除”并不是真正的語(yǔ)言層面擦除的,正如我們已經(jīng)看到的,這一切仍然是在 C++ 的類(lèi)型檢查系統(tǒng)中工作,維持著類(lèi)型安全上的優(yōu)點(diǎn)。