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