作者:唐風
關于類型擦除,在網上搜出來的中文資料比較少,而且一提到類型擦除,檢索結果里就跑出很多 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++ 的類型檢查系統中工作,維持著類型安全上的優點。