C++11模版元編程的應用


1.概述

  關於C++11模板元的基本用法和常用技巧,我在程序員2015年2月B《C++11模版元編程》一文(后稱前文)中已經做了詳細地介紹,那么C++11模版元編程用來解決什么實際問題呢,在實際工程中又該如何應用呢?本文將側重介紹C++11模板的一些具體應用,向讀者展示模版元編程的具體應用。

  我們將展示如何通過C++11模版元來實現function_traits、Vairant類型和泛型bind綁定器。function_traits側重於如何萃取可調用對象的一些元信息,Variant則是一種能接受多種類型數據的“萬能”類型,bind則是一個泛化的綁定器,下面來看看這些具體的例子。

2.function_traits

  function_traits用來獲取函數語義的可調用對象的一些屬性,比如函數類型、返回類型、函數指針類型和參數類型等。下面來看看如何實現function_traits。

template<typename T>
struct function_traits;

//普通函數
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;
    };
};

//函數指針
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)

//函數對象
template<typename Callable>
struct function_traits : function_traits<decltype(&Callable::operator())>{};

        由於可調用對象可能是普通的函數、函數指針、lambda、std::function和成員函數,所以我們需要針對這些類型分別做偏特化,然后萃取出可調用對象的元信息。其中,成員函數的偏特化稍微復雜一點,因為涉及到cv符的處理,這里通過定義一個宏來消除重復的模板類定義。參數類型的獲取我們是借助於tuple,將參數轉換為tuple類型,然后根據索引來獲取對應類型。它的用法比較簡單:

template<typename T>
void PrintType()
{
    cout << typeid(T).name() << endl;
}
int main()
{
    std::function<int(int)> f = [](int a){return a; };

    //打印函數類型
    PrintType<function_traits<std::function<int(int)>>::function_type>(); //將輸出int __cdecl(int)

    //打印函數的第一個參數類型
    PrintType<function_traits<std::function<int(int)>>::args<0>::type>();//將輸出int

    //打印函數的返回類型
  PrintType<function_traits<decltype(f)>::return_type>(); //將輸出int

  //打印函數指針類型
    PrintType<function_traits<decltype(f)>::pointer>(); //將輸出int (__cdecl*)(int)
}

  可以看到這個function_traits通過類型萃取,可以很方便地獲取可調用對象(函數、函數指針、函數對象、std::function和lambda表達式)的一些元信息,功能非常強大,這個function_traits經常會用到是更高層模版元程序的基礎。比如Variant類型的實現就要用到這個function_traits,下面來看看Variant的實現。

3.Variant

  借助上面的function_traits和前文實現的一些元函數,我們就能方便的實現一個“萬能類型”—Variant,Variant實際上一個泛化的類型,這個Variant和boost.variant的用法類似,需要預定義一些類型作為可接受的類型。boost.variant的基本用法如下:

typedef variant<int,char, double> vt;
vt v = 1;
v = 'a';
v = 12.32;

  這個variant可以接受已經定義的那些類型,看起來有點類似於c#和java中的object類型,實際上variant是擦除了類型,要獲取它的實際類型的時候就稍顯麻煩,需要通過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模版元實現的Variant將改進值的獲取,將獲取實際值的方式改為內置的,即通過下面的方式來訪問:

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; });//結果將輸出10

  這種方式更方便直觀。Variant的實現需要借助前文中實現的一些元函數MaxInteger、MaxAlign、Contains和At等等。下面來看看Variant實現的關鍵代碼,完整的代碼請讀者參考筆者在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));
        else
            Visit(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
};

  實現Variant首先需要定義一個足夠大的緩沖區用來存放不同的類型的值,這個緩類型沖區實際上就是用來擦除類型,不同的類型都通過placement new在這個緩沖區上創建對象,因為類型長度不同,所以需要考慮內存對齊,C++11剛好提供了內存對齊的緩沖區aligned_storage:

template< std::size_t Len, std::size_t Align = /*default-alignment*/ >
struct aligned_storage;

  它的第一個參數是緩沖區的長度,第二個參數是緩沖區內存對齊的大小,由於Varaint可以接受多種類型,所以我們需要獲取最大的類型長度,保證緩沖區足夠大,然后還要獲取最大的內存對齊大小,這里我們通過前面實現的MaxInteger和MaxAlign就可以了,Varaint中內存對齊的緩沖區定義如下:

enum
{
        data_size = IntegerMax<sizeof(Types)...>::value,
        align_size = MaxAlign<Types...>::value
};

using data_t = typename std::aligned_storage<data_size, align_size>::type; //內存對齊的緩沖區類型

  其次,我們還要實現對緩沖區的構造、拷貝、析構和移動,因為Variant重新賦值的時候需要將緩沖區中原來的類型析構掉,拷貝構造和移動構造時則需要拷貝和移動。這里以析構為例,我們需要根據當前的type_index來遍歷Variant的所有類型,找到對應的類型然后調用該類型的析構函數。

   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();
    }

  這里,我們通過初始化列表和逗號表達式來展開可變模板參數,在展開的過程中查找對應的類型,如果找到了則析構。在Variant構造時還需要注意一個細節是,Variant不能接受沒有預先定義的類型,所以在構造Variant時,需要限定類型必須在預定義的類型范圍當中,這里通過type_traits的enable_if來限定模板參數的類型。

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的條件就是前面實現的元函數Contains的值,當沒有在預定義的類型中找到對應的類型時,即Contains返回false時,編譯期會報一個編譯錯誤。

最后還需要實現內置的Vistit功能,Visit的實現需要先通過定義一系列的訪問函數,然后再遍歷這些函數,遍歷過程中,判斷函數的第一個參數類型的type_index是否與當前的type_index相同,如果相同則獲取當前類型的值。

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));
        else
            Visit(std::forward<Rest>(rest)...);
    }

  Visit功能的實現利用了可變模板參數和function_traits,通過可變模板參數來遍歷一系列的訪問函數,遍歷過程中,通過function_traits來獲取第一個參數的類型,和Variant當前的type_index相同時則取值。為什么要獲取訪問函數第一個參數的類型呢?因為Variant的值是唯一的,只有一個值,所以獲取的訪問函數的第一個參數的類型就是Variant中存儲的對象的實際類型。

4.bind

  C++11中新增的std::bind是一個很靈活且功能強大的綁定器,std::bind用來將可調用對象與其參數進行綁定。綁定后的結果可以使用std::function進行保存,並延遲調用到任何我們需要的時候。下面是它的基本用法:

void output(int x, int y)
{
    std::cout << x << " " << y << std::endl;
}

int main(void)
{
    std::bind(output, 1, 2)();                          // 輸出: 1 2
    std::bind(output, std::placeholders::_1, 2)(1);     // 輸出: 1 2
    std::bind(output, 2, std::placeholders::_1)(1);     // 輸出: 2 1
}

  std::placeholders::_1是一個占位符,代表這個位置將在函數調用時,被傳入的第一個參數所替代。因為有了占位符的概念,std::bind的使用非常靈活,我們可以用它來替代任意位置的參數,延遲到后面再傳入實際參數。下圖是bind的一個原理圖,更多的原理圖讀者可以參考:http://blog.think-async.com/2010/04/bind-illustrated.html

  從上圖中可以看到bind把參數和占位符保存起來了,然后在后面調用的時候再按照順序去替換占位符,最終實現延遲執行。

我們可以通過模板元來實現一個簡單的bind,實現bind需要解決兩個問題:

1.將tuple展開為可變模板參數

  bind綁定可調用對象時,需要將可調用對象的形參(可能含占位符)保存起來,保存到tuple中了。到了調用階段,我們就要反過來將tuple展開為可變參數,因為這個可變參數才是可調用對象的形參,否則就無法實現調用了。這里我們會借助於一個整形序列來將tuple變為可變參數,在展開tuple的過程中我們還需要根據占位符來選擇合適實參,即占位符要替換為調用實參。這里要用到前文中實現的MakeIndexes。

2.根據占位符來選擇合適的實參

  這個地方比較關鍵,因為tuple中可能含有占位符,我們展開tuple時,如果發現某個元素類型為占位符,則從調用的實參生成的tuple中取出一個實參,用來作為變參的一個參數;當某個類型不為占位符時,則直接從綁定時生成的形參tuple中取出參數,用來作為變參的一個參數。最終tuple被展開為一個變參列表,這時,這個列表中沒有占位符了,全是實參,就可以實現調用了。這里還有一個細節要注意,替換占位符的時候,如何從tuple中選擇合適的參數呢,因為替換的時候要根據順序來選擇。這里是通過占位符的模板參數I來選擇,因為占位符place_holder<I>的實例_1實際上place_holder<1>, 占位符實例_2實際上是palce_holder<2>,我們是可以根據占位符的模板參數來獲取其順序的。

  下面來看看bind實現的關鍵代碼,完整的代碼讀者可以參考我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...);
}
測試代碼:
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實現的關鍵技術,很多的實現細節並沒有處理,比如參數是否是引用、右值、cv符、綁定非靜態的成員變量都還沒處理,僅僅用來展示如何綜合運用一些模版元技巧和元函數,並非是重復發明輪子,只是展示bind是如何實現, 實際項目中還是使用c++11的std::bind為好。

5總結

  可以看到C++11模板元編程能解決很復雜的問題,能實現一些幾乎不可能完成的功能,比如實現了“萬能”類型Variant,和泛化的綁定器bind,這些東西的實現如果不通過模版元的話,幾乎是無法想象的。雖然這些應用看起來比較復雜,但是它將復雜性徹底的隱藏起來了,通過眼花繚亂的模版元技巧來解決復雜的問題,並最終給用戶提供簡單、強大和靈活的接口,這也是模版元編程最有魅力也最令人着迷的地方。模版元編程的應用很廣,本文只是展示了一小部分的應用,如果讀者還希望了解更多的應用,讀還可以參考boost的mpl庫和C++11的源碼。

 

備注:本文是我發表在《程序員》2015.3月A,轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM