Wrote by mutouyun. (http://darkc.at/cxx-type-list/)
群裡有個朋友要實現這麼一個功能:如何在編譯期把一個函數類型的參數減少一個。
簡單來說,就是實現下面這個模板:
remove_func_par<2, void(int, long, short)>::type; // type = void(int, long)
// make function's parameters from the types templatestruct make_func_par; template struct make_func_par > { typedef R type(P...); }; // remove function's parameter template struct remove_func_par; template struct remove_func_par { using erase_pars_t = typename types_erase , N>::type; using type = typename make_func_par ::type type; };
templatestruct types {};
如果定義了一組對types類型做操作的算法,那麼我們就可以把參數包放入types中,然後對它做這樣那樣的事情。。
看到這裡,不知道有沒有朋友想起來很久很久以前,Loki庫裡的TypeList。現代的C++當然不需要再像當年那樣用外敷類和繁瑣的宏來實現這個,使用變參模板加模板元就好了。
有了上面types的定義之後,下面需要實現一些算法來操作它。首先,在不涉及到容器的查找修改時,最基本的算法簡單來說有下面幾個:判斷容器類型(因為容器是編譯期的一個類型)、計算容器大小、判斷容器是否是空的。下面我們來依次實現它們。
判斷算法非常簡單:
/* Is types */ templatestruct is_types : std::false_type {}; template struct is_types > : std::true_type {};
// Check is types or not templatestruct check_is_types { static_assert(is_types ::value, "The template parameter is not a types-list!"); };
/* Return size */ templatestruct types_size : std::integral_constant , check_is_types {}; template struct types_size > : std::integral_constant {};
// Check is index valid or not templatestruct check_is_index_valid { static_assert(IndexN >= 0, "Index is out of range!"); static_assert(IndexN < types_size ::value, "Index is out of range!"); }; // Check is count valid or not template struct check_is_count_valid { static_assert(CountN > 0, "Count is too small!"); static_assert(CountN <= types_size ::value, "Count is too large!"); };
check_is_index_valid用來判斷傳入的索引是否超出了指定types的范圍;
check_is_count_valid用來判斷傳入的大小是否超出了指定types的大小。
和check_is_types一樣,在需要的時候繼承這兩個類模板就可以了。
然後,是容器是否為空的判斷:
/* Test whether types is empty */ templatestruct types_empty : std::true_type , check_is_types {}; template struct types_empty > : std::false_type {}; template <> struct types_empty > : std::true_type {};
types的訪問算法就是根據傳入的索引(index)定位類型。我們可以先寫下types_at的定義:
templatestruct types_at : check_is_index_valid { using type = TypesT; };
templatestruct types_at , N> : types_at , N - 1> {}; template struct types_at , 0> { using type = T1; };
上面的第一個types_at特化負責把參數包和index同時減1,並傳入下一層;最後模板的遞歸會在第二個types_at特化處終結。
我們看到,這裡並不需要一個types<>的特化。因為當傳入的模板參數是types<>的時候,它不會匹配到任何一個特化,因此最初的types_at定義就可以搞定這種情況了。
有了types_at之後,我們可以很方便的實現front和back的定位算法:
/* Access first element */ templatestruct types_front { using type = types_at_t ; }; /* Access last element */ template struct types_back { using type = types_at_t ::value - 1>; };
這兩個算法都是用來把類型打包成types的。
首先我們來考慮類型的連接。需求很簡單,傳入兩個類型,把它們連接成一個types。
當參數是普通類型時的算法很簡單:
templatestruct types_link { using type = types ; };
templatestruct types_link , U> { using type = types ; }; template struct types_link > { using type = types ; };
templatestruct types_link , types > { using type = types ; };
我們注意到,上面的link算法裡考慮了當參數是types的情況。因此在做後面的其它算法時,通過使用這裡的link,會把types內部的types展開。
下面是types的Assign算法。需求是,傳入一個數字N和類型T,types_assign將構造一個由N個T組成的types。
有了上面的types_link以後,我們可以在模板遞歸中一次連接一個T,直到N減少到0為止。算法如下:
templatestruct types_assign { static_assert(N >= 0, "N cannot be less than 0!"); private: using tail = typename types_assign ::type; public: using type = typename types_link ::type; }; template struct types_assign<0, T> { using type = types<>; };
由於使用了types_link連接types,當我們這樣寫時:types_assign<2, types
插入算法的需求如下:
給定一個types,傳入索引index和類型T,需要把T插入到types的index處。根據這個需求,我們可以先寫出types_insert的定義:
templatestruct types_insert : check_is_types , check_is_index_valid { using type = TypesT; };
templatestruct types_insert , N, U> { private: using tail = typename types_insert , N - 1, U>::type; public: using type = typename types_link ::type; };
templatestruct types_insert , 0, U> { using type = typename types_link>::type; };
templatestruct types_insert , 0, U> { using type = typename types_link>::type; };
因為若不添加這個特化的話,types<>會被匹配到types_insert的定義上去,那麼types<>將無法插入任何類型了。
可能有童鞋看到這裡,覺得我們沒必要把types
templatestruct types_insert , 0, U> { using type = typename types_link>::type; };
看起來好像沒問題,但實際上是不行的。這是因為
而
這裡也說明了模板元編程時書寫的一個原則:應該從最普遍的特化版本開始,逐一特殊化各種條件,直到最後的遞歸終結。
這種書寫方法可以保證不會出現模板特化的二義性,只是和數學歸納法的思考方向相反。如果習慣於用數學歸納法之類的方式思考模板元遞歸算法的童鞋,可以先正著寫出算法,再倒著看每個條件是否是逐步特殊化的。
下面我們思考刪除算法。需求:
給定一個types,傳入索引index和數量count,需要把types中從索引index處開始的count個元素刪除。
首先,我們還是先寫出定義:
templatestruct types_erase : check_is_types , check_is_index_valid { using type = TypesT; };
templatestruct types_erase , N, C> { private: using tail = typename types_erase , N - 1, C>::type; public: using type = typename types_link ::type; };
templatestruct types_erase , 0, 1> { using type = types ; };
templatestruct types_erase , 0, C> : check_is_count_valid , C> { using type = typename types_erase , 0, C - 1>::type; };
, N, C> , 0, C> , 0, 1>
那麼是否所有的情況都考慮到了呢?通過枚舉出所有的特化條件,我們發現只有types<>沒有考慮。對於types_erase來說,types<>沒有刪除的意義,因此直接讓它匹配到types_erase的定義就可以了。當然,這會引起一個編譯期的static_assert,因為任何的index都將超出types<>的范圍。
查找算法的需求如下:
給定一個types和類型T,需要在types中找到T所在的第一個索引位置。
首先,我們先寫出定義:
templatestruct types_find : std::integral_constant , check_is_types {};
接著,我們用數學歸納法的方式來思考:
當types中的第一個元素為T時,索引位置為0;(終結條件)
當types中的第N個元素為T時,索引位置為上一個元素的索引加1。
那麼我們可以先列出需要特化的版本:
, T1> , U>
templatestruct types_find , T1> : std::integral_constant {};
templatestruct types_find , U> : std::integral_constant , U>::value == -1 ? -1 : types_find , U>::value + 1)> {};
templatestruct types_exist : std::integral_constant ::value != -1)> {};
templateclass If_, typename V, template class Do_, typename U> struct types_do_if : check_is_types { using type = TypesT; };
using done = typename Do_::value, U, T1>::type;
templateclass If_, typename V, template class Do_, typename U> struct types_do_if , If_, V, Do_, U> { private: using tail = typename types_do_if , If_, V, Do_, U>::type; using done = typename Do_ ::value, U, T1>::type; public: using type = typename types_link
費這麼大勁寫這個一般化的算法有什麼用呢?下面我們來看看它的威力。
首先,是types的置換算法:
給定一個types,以及類型T,U;要求把所有types中的T都換成U。
有了上面的types_do_if,實現這個算法非常輕松:
templatestruct types_replace : types_do_if {};
當在types中找到類型T的時候,就把它變成U。代碼和語言描述基本是一致的。
接下來,考慮一個移除的算法:
給定一個types,和類型T,要求從types中移除所有的T。
通過types_do_if實現如下:
templatestruct types_remove : types_do_if > {};
templatestruct types_remove : types_replace > {};
templatestruct types_remove > { private: using rm_t = typename types_remove ::type; public: using type = typename types_remove >::type; };
從types
通過types_do_if還可以實現很多特殊操作,在這裡就不再展開了。
接下來,我們實現types的“壓縮”算法。當types裡有多個重復元素的時候,如何把重復的內容剔除掉,只保留一個呢?
同樣的,我們先寫出定義:
templatestruct types_compact : check_is_types { using type = TypesT; };
templatestruct types_compact > { private: using rm_t = typename types_remove , T1>::type; using tail = typename types_compact ::type; public: using type = typename types_link ::type; };
templatestruct types_reverse : check_is_types { using type = TypesT; }; template struct types_reverse > { private: using head = typename types_reverse >::type; public: using type = typename types_link::type; };
每次取出第一個元素,然後把它放到最後面即可。
在編譯期排序和運行期其實並沒什麼不同,只是算法的選擇上需要考慮一下。假設是從大到小排列,那麼最直觀的想法是每次遞歸都從types中找到最大的元素,然後把它放到頭上去。這樣遞歸完畢後整個types就是有序的了。
這種想法其實就是選擇排序(Selection sort)。
當然,我們也可以實現插入,或者快排。如果讀者感興趣的話,可以自己實現一下。
使用選擇排序,首先需要能從types中找到放在最前面的那個元素。在這裡我們不使用現成的比較算法,而寫成可以讓外部指定比較算法。那麼select的算法定義如下:
templateclass If_> struct types_select_if : check_is_types { using type = TypesT; };
我們先用數學歸納法思考下算法:
當types中只有1個元素T1時,直接返回T1;(終結條件)
當types中有1個元素以上時,先得到T1以外的其它元素的select結果(S),然後將T1和S一起放入If_中。若If_為true,那麼選擇T1,否則選擇S。
同樣,先列出特化條件:
, If_> , If_>
templateclass If_> struct types_select_if , If_> { using type = T1; }; template class If_> struct types_select_if , If_> { private: using select_t = typename types_select_if , If_>::type; public: using type = typename std::conditional ::value, T1, select_t>::type; };
templateclass If_> struct types_sort_if : check_is_types { using type = TypesT; };
和上面一樣,先用數學歸納法思考下:
當types中只有1個元素T1時,直接返回types
當types中有1個元素以上時,先得到types的select結果(S),之後從types中刪除S,然後對結果遞歸運算,最後把S連接到頭部。
列出特化條件:
, If_> , If_>
templateclass If_> struct types_sort_if , If_> { using type = types ; }; template class If_> struct types_sort_if , If_> { private: using types_t = types ; using sl_t = typename types_select_if ::type; using er_t = typename types_erase ::value>::type; using tail = typename types_sort_if ::type; public: using type = typename types_link ::type; };
using types_t = types; template struct is_large : std::integral_constant sizeof(U))> {}; using sort_t = types_sort_if ::type; // sort_t = types
實際項目中,我們往往不會像這樣寫這麼多模板元的代碼。如果有類似需求,可能會考慮直接使用Boost.MPL,或者在Loki.TypeList的基礎上加一層變參模板的外敷。
自己完整的實現一次模板元的容器操作算法的意義,在於可以大大加深對模板元編程,以及對變參模板的理解。
有了這些經驗之後,在不方便使用第三方庫時,能夠快速自撸一些簡單且可靠的模板元算法,來完成一些編譯期計算的需求;同時也可以幫助我們更清晰的理解和分析一些C++模板庫(STL、Boost之類)裡的泛型算法。
另外,目前的std::tuple的實現方式其實是類似上面的types的。比如gnuc的libstdc++裡的定義:
// Forward declarations. templateclass tuple;
而目前stl裡對std::tuple的編譯期操作很簡單,只有std::tuple_size和std::tuple_element兩種。如果想增加std::tuple的編譯期運算功能,也可以自行采用上面類似的算法做拓展。
完整代碼及測試下載請點擊:types
Wrote by mutouyun. (http://darkc.at/cxx-type-list/)