C++11模版元编程的应用
1.概述
關(guān)于C++11模板元的基本用法和常用技巧,我在程序員2015年2月B《C++11模版元編程》一文(后稱前文)中已經(jīng)做了詳細(xì)地介紹,那么C++11模版元編程用來解決什么實(shí)際問題呢,在實(shí)際工程中又該如何應(yīng)用呢?本文將側(cè)重介紹C++11模板的一些具體應(yīng)用,向讀者展示模版元編程的具體應(yīng)用。
我們將展示如何通過C++11模版元來實(shí)現(xiàn)function_traits、Vairant類型和泛型bind綁定器。function_traits側(cè)重于如何萃取可調(diào)用對(duì)象的一些元信息,Variant則是一種能接受多種類型數(shù)據(jù)的“萬(wàn)能”類型,bind則是一個(gè)泛化的綁定器,下面來看看這些具體的例子。
2.function_traits
function_traits用來獲取函數(shù)語(yǔ)義的可調(diào)用對(duì)象的一些屬性,比如函數(shù)類型、返回類型、函數(shù)指針類型和參數(shù)類型等。下面來看看如何實(shí)現(xiàn)function_traits。
template<typename T> struct function_traits;//普通函數(shù) template<typename Ret, typename... Args> struct function_traits<Ret(Args...)> { public:enum { arity = sizeof...(Args) };typedef Ret function_type(Args...);typedef Ret return_type;using stl_function_type = std::function<function_type>;typedef Ret(*pointer)(Args...);template<size_t I>struct args{static_assert(I < arity, "index is out of range, index must less than sizeof Args");using type = typename std::tuple_element<I, std::tuple<Args...>>::type;}; };//函數(shù)指針 template<typename Ret, typename... Args> struct function_traits<Ret(*)(Args...)> : function_traits<Ret(Args...)>{};//std::function template <typename Ret, typename... Args> struct function_traits<std::function<Ret(Args...)>> : function_traits<Ret(Args...)>{};//member function #define FUNCTION_TRAITS(...) \template <typename ReturnType, typename ClassType, typename... Args>\struct function_traits<ReturnType(ClassType::*)(Args...) __VA_ARGS__> : function_traits<ReturnType(Args...)>{}; \FUNCTION_TRAITS() FUNCTION_TRAITS(const) FUNCTION_TRAITS(volatile) FUNCTION_TRAITS(const volatile)//函數(shù)對(duì)象 template<typename Callable> struct function_traits : function_traits<decltype(&Callable::operator())>{};??????? 由于可調(diào)用對(duì)象可能是普通的函數(shù)、函數(shù)指針、lambda、std::function和成員函數(shù),所以我們需要針對(duì)這些類型分別做偏特化,然后萃取出可調(diào)用對(duì)象的元信息。其中,成員函數(shù)的偏特化稍微復(fù)雜一點(diǎn),因?yàn)樯婕暗絚v符的處理,這里通過定義一個(gè)宏來消除重復(fù)的模板類定義。參數(shù)類型的獲取我們是借助于tuple,將參數(shù)轉(zhuǎn)換為tuple類型,然后根據(jù)索引來獲取對(duì)應(yīng)類型。它的用法比較簡(jiǎn)單:
template<typename T> void PrintType() {cout << typeid(T).name() << endl; } int main() {std::function<int(int)> f = [](int a){return a; };//打印函數(shù)類型PrintType<function_traits<std::function<int(int)>>::function_type>(); //將輸出int __cdecl(int)//打印函數(shù)的第一個(gè)參數(shù)類型PrintType<function_traits<std::function<int(int)>>::args<0>::type>();//將輸出int//打印函數(shù)的返回類型PrintType<function_traits<decltype(f)>::return_type>(); //將輸出int//打印函數(shù)指針類型PrintType<function_traits<decltype(f)>::pointer>(); //將輸出int (__cdecl*)(int) }可以看到這個(gè)function_traits通過類型萃取,可以很方便地獲取可調(diào)用對(duì)象(函數(shù)、函數(shù)指針、函數(shù)對(duì)象、std::function和lambda表達(dá)式)的一些元信息,功能非常強(qiáng)大,這個(gè)function_traits經(jīng)常會(huì)用到是更高層模版元程序的基礎(chǔ)。比如Variant類型的實(shí)現(xiàn)就要用到這個(gè)function_traits,下面來看看Variant的實(shí)現(xiàn)。
3.Variant
借助上面的function_traits和前文實(shí)現(xiàn)的一些元函數(shù),我們就能方便的實(shí)現(xiàn)一個(gè)“萬(wàn)能類型”—Variant,Variant實(shí)際上一個(gè)泛化的類型,這個(gè)Variant和boost.variant的用法類似,需要預(yù)定義一些類型作為可接受的類型。boost.variant的基本用法如下:
typedef variant<int,char, double> vt; vt v = 1; v = 'a'; v = 12.32;這個(gè)variant可以接受已經(jīng)定義的那些類型,看起來有點(diǎn)類似于c#和java中的object類型,實(shí)際上variant是擦除了類型,要獲取它的實(shí)際類型的時(shí)候就稍顯麻煩,需要通過boost.visitor來訪問:
struct VariantVisitor : public boost::static_visitor<void> {void operator() (int a){cout << "int" << endl;}void operator() (short val){cout << "short" << endl;}void operator() (double val){cout << "double" << endl;}void operator() (std::string val){cout << "string" << endl;} };boost::variant<int,short,double,std::string> v = 1; boost::apply_visitor(visitor, v); //將輸出int通過C++11模版元實(shí)現(xiàn)的Variant將改進(jìn)值的獲取,將獲取實(shí)際值的方式改為內(nèi)置的,即通過下面的方式來訪問:
typedef Variant<int, double, string, int> cv; cv v = 10; v.Visit([&](double i){cout << i << endl; }, [](short i){cout << i << endl; }, [=](int i){cout << i << endl; },[](const string& i){cout << i << endl; });//結(jié)果將輸出10這種方式更方便直觀。Variant的實(shí)現(xiàn)需要借助前文中實(shí)現(xiàn)的一些元函數(shù)MaxInteger、MaxAlign、Contains和At等等。下面來看看Variant實(shí)現(xiàn)的關(guān)鍵代碼,完整的代碼請(qǐng)讀者參考筆者在github上的代碼https://github.com/qicosmos/cosmos/blob/master/Varaint.hpp。
template<typename... Types> class Variant{enum{data_size = IntegerMax<sizeof(Types)...>::value,align_size = MaxAlign<Types...>::value};using data_t = typename std::aligned_storage<data_size, align_size>::type; public:template<int index>using IndexType = typename At<index, Types...>::type;Variant(void) :m_typeIndex(typeid(void)){}~Variant(){ Destroy(m_typeIndex, &m_data); }Variant(Variant<Types...>&& old) : m_typeIndex(old.m_typeIndex){Move(old.m_typeIndex, &old.m_data, &m_data);}Variant(const Variant<Types...>& old) : m_typeIndex(old.m_typeIndex){Copy(old.m_typeIndex, &old.m_data, &m_data);}template <class T,class = typename std::enable_if<Contains<typename std::remove_reference<T>::type, Types...>::value>::type> Variant(T&& value) : m_typeIndex(typeid(void)){Destroy(m_typeIndex, &m_data);typedef typename std::remove_reference<T>::type U;new(&m_data) U(std::forward<T>(value));m_typeIndex = type_index(typeid(U));}template<typename T>bool Is() const{return (m_typeIndex == type_index(typeid(T)));}template<typename T>typename std::decay<T>::type& Get(){using U = typename std::decay<T>::type;if (!Is<U>()){cout << typeid(U).name() << " is not defined. " << "current type is " <<m_typeIndex.name() << endl;throw std::bad_cast();}return *(U*)(&m_data);}template<typename F>void Visit(F&& f){using T = typename Function_Traits<F>::template arg<0>::type;if (Is<T>())f(Get<T>());}template<typename F, typename... Rest>void Visit(F&& f, Rest&&... rest){using T = typename Function_Traits<F>::template arg<0>::type;if (Is<T>())Visit(std::forward<F>(f));elseVisit(std::forward<Rest>(rest)...);} private:void Destroy(const type_index& index, void * buf){std::initializer_list<int>{(Destroy0<Types>(index, buf), 0)...};}template<typename T>void Destroy0(const type_index& id, void* data){if (id == type_index(typeid(T)))reinterpret_cast<T*>(data)->~T();}void Move(const type_index& old_t, void* old_v, void* new_v) {std::initializer_list<int>{(Move0<Types>(old_t, old_v, new_v), 0)...};}template<typename T>void Move0(const type_index& old_t, void* old_v, void* new_v){if (old_t == type_index(typeid(T)))new (new_v)T(std::move(*reinterpret_cast<T*>(old_v)));}void Copy(const type_index& old_t, void* old_v, void* new_v){std::initializer_list<int>{(Copy0<Types>(old_t, old_v, new_v), 0)...};}template<typename T>void Copy0(const type_index& old_t, void* old_v, void* new_v){if (old_t == type_index(typeid(T)))new (new_v)T(*reinterpret_cast<const T*>(old_v));} private:data_t m_data;std::type_index m_typeIndex;//類型ID };實(shí)現(xiàn)Variant首先需要定義一個(gè)足夠大的緩沖區(qū)用來存放不同的類型的值,這個(gè)緩類型沖區(qū)實(shí)際上就是用來擦除類型,不同的類型都通過placement new在這個(gè)緩沖區(qū)上創(chuàng)建對(duì)象,因?yàn)轭愋烷L(zhǎng)度不同,所以需要考慮內(nèi)存對(duì)齊,C++11剛好提供了內(nèi)存對(duì)齊的緩沖區(qū)aligned_storage:
template< std::size_t Len, std::size_t Align = /*default-alignment*/ > struct aligned_storage;它的第一個(gè)參數(shù)是緩沖區(qū)的長(zhǎng)度,第二個(gè)參數(shù)是緩沖區(qū)內(nèi)存對(duì)齊的大小,由于Varaint可以接受多種類型,所以我們需要獲取最大的類型長(zhǎng)度,保證緩沖區(qū)足夠大,然后還要獲取最大的內(nèi)存對(duì)齊大小,這里我們通過前面實(shí)現(xiàn)的MaxInteger和MaxAlign就可以了,Varaint中內(nèi)存對(duì)齊的緩沖區(qū)定義如下:
enum {data_size = IntegerMax<sizeof(Types)...>::value,align_size = MaxAlign<Types...>::value };using data_t = typename std::aligned_storage<data_size, align_size>::type; //內(nèi)存對(duì)齊的緩沖區(qū)類型其次,我們還要實(shí)現(xiàn)對(duì)緩沖區(qū)的構(gòu)造、拷貝、析構(gòu)和移動(dòng),因?yàn)閂ariant重新賦值的時(shí)候需要將緩沖區(qū)中原來的類型析構(gòu)掉,拷貝構(gòu)造和移動(dòng)構(gòu)造時(shí)則需要拷貝和移動(dòng)。這里以析構(gòu)為例,我們需要根據(jù)當(dāng)前的type_index來遍歷Variant的所有類型,找到對(duì)應(yīng)的類型然后調(diào)用該類型的析構(gòu)函數(shù)。
void Destroy(const type_index& index, void * buf){std::initializer_list<int>{(Destroy0<Types>(index, buf), 0)...};}template<typename T>void Destroy0(const type_index& id, void* data){if (id == type_index(typeid(T)))reinterpret_cast<T*>(data)->~T();}這里,我們通過初始化列表和逗號(hào)表達(dá)式來展開可變模板參數(shù),在展開的過程中查找對(duì)應(yīng)的類型,如果找到了則析構(gòu)。在Variant構(gòu)造時(shí)還需要注意一個(gè)細(xì)節(jié)是,Variant不能接受沒有預(yù)先定義的類型,所以在構(gòu)造Variant時(shí),需要限定類型必須在預(yù)定義的類型范圍當(dāng)中,這里通過type_traits的enable_if來限定模板參數(shù)的類型。
template <class T,class = typename std::enable_if<Contains<typename std::remove_reference<T>::type, Types...>::value>::type> Variant(T&& value) : m_typeIndex(typeid(void)){Destroy(m_typeIndex, &m_data);typedef typename std::remove_reference<T>::type U;new(&m_data) U(std::forward<T>(value));m_typeIndex = type_index(typeid(U));}這里enbale_if的條件就是前面實(shí)現(xiàn)的元函數(shù)Contains的值,當(dāng)沒有在預(yù)定義的類型中找到對(duì)應(yīng)的類型時(shí),即Contains返回false時(shí),編譯期會(huì)報(bào)一個(gè)編譯錯(cuò)誤。
最后還需要實(shí)現(xiàn)內(nèi)置的Vistit功能,Visit的實(shí)現(xiàn)需要先通過定義一系列的訪問函數(shù),然后再遍歷這些函數(shù),遍歷過程中,判斷函數(shù)的第一個(gè)參數(shù)類型的type_index是否與當(dāng)前的type_index相同,如果相同則獲取當(dāng)前類型的值。
template<typename F>void Visit(F&& f){using T = typename Function_Traits<F>::template arg<0>::type;if (Is<T>())f(Get<T>());}template<typename F, typename... Rest>void Visit(F&& f, Rest&&... rest){using T = typename Function_Traits<F>::template arg<0>::type;if (Is<T>())Visit(std::forward<F>(f));elseVisit(std::forward<Rest>(rest)...);}Visit功能的實(shí)現(xiàn)利用了可變模板參數(shù)和function_traits,通過可變模板參數(shù)來遍歷一系列的訪問函數(shù),遍歷過程中,通過function_traits來獲取第一個(gè)參數(shù)的類型,和Variant當(dāng)前的type_index相同時(shí)則取值。為什么要獲取訪問函數(shù)第一個(gè)參數(shù)的類型呢?因?yàn)閂ariant的值是唯一的,只有一個(gè)值,所以獲取的訪問函數(shù)的第一個(gè)參數(shù)的類型就是Variant中存儲(chǔ)的對(duì)象的實(shí)際類型。
4.bind
C++11中新增的std::bind是一個(gè)很靈活且功能強(qiáng)大的綁定器,std::bind用來將可調(diào)用對(duì)象與其參數(shù)進(jìn)行綁定。綁定后的結(jié)果可以使用std::function進(jìn)行保存,并延遲調(diào)用到任何我們需要的時(shí)候。下面是它的基本用法:
void output(int x, int y) {std::cout << x << " " << y << std::endl; }int main(void) {std::bind(output, 1, 2)(); // 輸出: 1 2std::bind(output, std::placeholders::_1, 2)(1); // 輸出: 1 2std::bind(output, 2, std::placeholders::_1)(1); // 輸出: 2 1 }std::placeholders::_1是一個(gè)占位符,代表這個(gè)位置將在函數(shù)調(diào)用時(shí),被傳入的第一個(gè)參數(shù)所替代。因?yàn)橛辛苏嘉环母拍?#xff0c;std::bind的使用非常靈活,我們可以用它來替代任意位置的參數(shù),延遲到后面再傳入實(shí)際參數(shù)。下圖是bind的一個(gè)原理圖,更多的原理圖讀者可以參考:http://blog.think-async.com/2010/04/bind-illustrated.html。
從上圖中可以看到bind把參數(shù)和占位符保存起來了,然后在后面調(diào)用的時(shí)候再按照順序去替換占位符,最終實(shí)現(xiàn)延遲執(zhí)行。
我們可以通過模板元來實(shí)現(xiàn)一個(gè)簡(jiǎn)單的bind,實(shí)現(xiàn)bind需要解決兩個(gè)問題:
1.將tuple展開為可變模板參數(shù)
bind綁定可調(diào)用對(duì)象時(shí),需要將可調(diào)用對(duì)象的形參(可能含占位符)保存起來,保存到tuple中了。到了調(diào)用階段,我們就要反過來將tuple展開為可變參數(shù),因?yàn)檫@個(gè)可變參數(shù)才是可調(diào)用對(duì)象的形參,否則就無(wú)法實(shí)現(xiàn)調(diào)用了。這里我們會(huì)借助于一個(gè)整形序列來將tuple變?yōu)榭勺儏?shù),在展開tuple的過程中我們還需要根據(jù)占位符來選擇合適實(shí)參,即占位符要替換為調(diào)用實(shí)參。這里要用到前文中實(shí)現(xiàn)的MakeIndexes。
2.根據(jù)占位符來選擇合適的實(shí)參
這個(gè)地方比較關(guān)鍵,因?yàn)閠uple中可能含有占位符,我們展開tuple時(shí),如果發(fā)現(xiàn)某個(gè)元素類型為占位符,則從調(diào)用的實(shí)參生成的tuple中取出一個(gè)實(shí)參,用來作為變參的一個(gè)參數(shù);當(dāng)某個(gè)類型不為占位符時(shí),則直接從綁定時(shí)生成的形參tuple中取出參數(shù),用來作為變參的一個(gè)參數(shù)。最終tuple被展開為一個(gè)變參列表,這時(shí),這個(gè)列表中沒有占位符了,全是實(shí)參,就可以實(shí)現(xiàn)調(diào)用了。這里還有一個(gè)細(xì)節(jié)要注意,替換占位符的時(shí)候,如何從tuple中選擇合適的參數(shù)呢,因?yàn)樘鎿Q的時(shí)候要根據(jù)順序來選擇。這里是通過占位符的模板參數(shù)I來選擇,因?yàn)檎嘉环鹥lace_holder<I>的實(shí)例_1實(shí)際上place_holder<1>, 占位符實(shí)例_2實(shí)際上是palce_holder<2>,我們是可以根據(jù)占位符的模板參數(shù)來獲取其順序的。
下面來看看bind實(shí)現(xiàn)的關(guān)鍵代碼,完整的代碼讀者可以參考我github上的代碼:https://github.com/qicosmos/cosmos/blob/master/Bind.hpp。
template <int I> struct Placeholder{};Placeholder<1> _1; Placeholder<2> _2; Placeholder<3> _3; Placeholder<4> _4; Placeholder<5>_5; Placeholder<6> _6; Placeholder<7> _7;Placeholder<8> _8; Placeholder<9> _9; Placeholder<10> _10;template <typename T, class Tuple> inline auto select(T&& val, Tuple&)->T&&{return std::forward<T>(val); }template <int I, class Tuple> inline auto select(Placeholder<I>&, Tuple& tp) -> decltype(std::get<I - 1>(tp)){return std::get<I - 1>(tp); }template <typename R, typename F, typename... P> inline typename std::enable_if<is_pointer_noref<F>::value, R>::type invoke(F&& f, P&&... par){return (*std::forward<F>(f))(std::forward<P>(par)...); }template<typename Fun, typename... Args> struct Bind_t{typedef typename decay<Fun>::type FunType;typedef std::tuple<typename decay<Args>::type...> ArgType;typedef typename function_traits<FunType>::return_type result_type; public:template<class F, class... BArgs>Bind_t(F& f, BArgs&... args) : m_func(f), m_args(args...){}template<typename F, typename... BArgs>Bind_t(F&& f, BArgs&&... par) : m_func(std::move(f)), m_args(std::move(par)...){}template <typename... CArgs>result_type operator()(CArgs&&... args){return do_call(MakeIndexes<std::tuple_size<ArgType>::value>::type(),std::forward_as_tuple(std::forward<CArgs>(args)...));}template<typename ArgTuple, int... Indexes >result_type do_call(IndexTuple< Indexes... >& in, ArgTuple& argtp){return invoke<result_type>(m_func, select(std::get<Indexes>(m_args),argtp)...);}private:FunType m_func;ArgType m_args; };template <typename F, typename... P> inline Bind_t<F, P...> Bind(F&& f, P&&... par){return Bind_t<F, P...>(std::forward<F>(f), std::forward<P>(par)...); }template <typename F, typename... P> inline Bind_t<F, P...> Bind(F& f, P&... par){return Bind_t<F, P...>(f, par...); } 測(cè)試代碼: void TestFun1(int a, int b, int c) { }void TestBind1() {Bind(&TestFun1, _1, _2, _3)(1, 2, 3);Bind(&TestFun1, 4, 5, _1)(6);Bind(&TestFun1, _1, 4, 5)(3);Bind(&TestFun1, 3, _1, 5)(4); }由于只是展示bind實(shí)現(xiàn)的關(guān)鍵技術(shù),很多的實(shí)現(xiàn)細(xì)節(jié)并沒有處理,比如參數(shù)是否是引用、右值、cv符、綁定非靜態(tài)的成員變量都還沒處理,僅僅用來展示如何綜合運(yùn)用一些模版元技巧和元函數(shù),并非是重復(fù)發(fā)明輪子,只是展示bind是如何實(shí)現(xiàn), 實(shí)際項(xiàng)目中還是使用c++11的std::bind為好。
5總結(jié)
可以看到C++11模板元編程能解決很復(fù)雜的問題,能實(shí)現(xiàn)一些幾乎不可能完成的功能,比如實(shí)現(xiàn)了“萬(wàn)能”類型Variant,和泛化的綁定器bind,這些東西的實(shí)現(xiàn)如果不通過模版元的話,幾乎是無(wú)法想象的。雖然這些應(yīng)用看起來比較復(fù)雜,但是它將復(fù)雜性徹底的隱藏起來了,通過眼花繚亂的模版元技巧來解決復(fù)雜的問題,并最終給用戶提供簡(jiǎn)單、強(qiáng)大和靈活的接口,這也是模版元編程最有魅力也最令人著迷的地方。模版元編程的應(yīng)用很廣,本文只是展示了一小部分的應(yīng)用,如果讀者還希望了解更多的應(yīng)用,讀還可以參考boost的mpl庫(kù)和C++11的源碼。
?
備注:本文是我發(fā)表在《程序員》2015.3月A,轉(zhuǎn)載請(qǐng)注明出處。
總結(jié)
以上是生活随笔為你收集整理的C++11模版元编程的应用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何强制关闭极域
- 下一篇: vc++出现warningC4819的处