C++11模版元編程


1.概述

  模版元編程(template metaprogram)是C++中最復雜也是威力最強大的編程范式,它是一種可以創建和操縱程序的程序。模版元編程完全不同於普通的運行期程序,它很獨特,因為模版元程序的執行完全是在編譯期,並且模版元程序操縱的數據不能是運行時變量,只能是編譯期常量,不可修改,另外它用到的語法元素也是相當有限,不能使用運行期的一些語法,比如if-else,for等語句都不能用。因此,模版元編程需要很多技巧,常常需要類型重定義、枚舉常量、繼承、模板偏特化等方法來配合,因此編寫模版元編程比較復雜也比較困難。

  現在C++11新增了一些模版元相關的特性,不僅可以讓我們編寫模版元程序變得更容易,還進一步增強了泛型編程的能力,比如type_traits讓我們不必再重復發明輪子了,給我們提供了大量便利的元函數,還提供了可變模板參數和tuple,讓模版元編程“如虎添翼”。本文將向讀者展示C++11中模版元編程常用的技巧和具體應用。

2.模版元基本概念

  模版元程序由元數據和元函數組成,元數據就是元編程可以操作的數據,即C++編譯器在編譯期可以操作的數據。元數據不是運行期變量,只能是編譯期常量,不能修改,常見的元數據有enum枚舉常量、靜態常量、基本類型和自定義類型等。

  元函數是模板元編程中用於操作處理元數據的“構件”,可以在編譯期被“調用”,因為它的功能和形式和運行時的函數類似,而被稱為元函數,它是元編程中最重要的構件。元函數實際上表現為C++的一個類、模板類或模板函數,它的通常形式如下:

template<int N, int M>
struct meta_func
{
    static const int value = N+M;
}

  調用元函數獲取value值:cout<<meta_func<1, 2>::value<<endl;

  meta_func的執行過程是在編譯期完成的,實際執行程序時,是沒有計算動作而是直接使用編譯期的計算結果的。元函數只處理元數據,元數據是編譯期常量和類型,所以下面的代碼是編譯不過的:

int i = 1, j = 2;
meta_func<i, j>::value; //錯誤,元函數無法處理運行時普通數據

  模板元編程產生的源程序是在編譯期執行的程序,因此它首先要遵循C++和模板的語法,但是它操作的對象不是運行時普通的變量,因此不能使用運行時的C++關鍵字(如if、else、for),可用的語法元素相當有限,最常用的是:

  • enum、static const,用來定義編譯期的整數常量;
  • typedef/using,用於定義元數據;
  • T、Args...,聲明元數據類型;
  • template,主要用於定義元函數;
  • "::",域運算符,用於解析類型作用域獲取計算結果(元數據)。

如果模板元編程中需要if-else、for等邏輯時該怎么辦呢?

模板元中的if-else可以通過type_traits來實現,它不僅僅可以在編譯期做判斷,還可以做計算、查詢、轉換和選擇。

模板元中的for等邏輯可以通過遞歸、重載、和模板特化(偏特化)等方法實現。

下面來看看C++11提供的模版元基礎庫type_traits。

3.type_traits

  type_traits是C++11提供的模板元基礎庫,通過type_traits可以實現在編譯期計算、查詢、判斷、轉換和選擇,提供了模板元編程需要的一些常用元函數。下面來看看一些基本的type_traits的基本用法。

  最簡單的一個type_traits是定義編譯期常量的元函數integral_constant,它的定義如下:

template< class T, T v >
struct integral_constant;

  借助這個簡單的trait,我們可以很方便地定義編譯期常量,比如定義一個值為1的int常量可以這樣定義:

using one_type = std::integral_constant<int, 1>;

或者

template<class T>
struct one_type : std::integral_constant<int, 1>{};

  獲取常量則通過one_type::value來獲取,這種定義編譯期常量的方式相比C++98/03要簡單,在C++98/03中定義編譯期常量一般是這樣定義的:

template<class T>
struct one_type
{
    enum{value = 1};
};

template<class T>
struct one_type
{
    static const int value = 1;
};

  可以看到,通過C++11的type_traits提供的一個簡單的integral_constant就可以很方便的定義編譯期常量,而無需再去通過定義enum和static const變量方式去定義編譯期常量了,這也為定義編譯期常量提供了另外一種方法。C++11的type_traits已經提供了編譯期的true和false,是通過integral_constant來定義的:

typedef  integral_constant<bool, true> true_type;
typedef  integral_constant<bool, false> false_type;

  除了這些基本的元函數之外,type_traits還提供了豐富的元函數,比如用於編譯期判斷的元函數:

  這只是列舉一小部分的type_traits元函數,type_traits提供了上百個方便的元函數,讀者可以參考http://en.cppreference.com/w/cpp/header/type_traits,這些基本的元函數用法比較簡單:

#include <iostream>
#include <type_traits>

int main() {
  std::cout << "int: " << std::is_const<int>::value << std::endl;
  std::cout << "const int: " << std::is_const<const int>::value << std::endl;

  //判斷類型是否相同
  std::cout<< std::is_same<int, int>::value<<"\n";// true
  std::cout<< std::is_same<int, unsignedint>::value<<"\n";// false

  //添加、移除const
  cout << std::is_same<const int, add_const<int>::type>::value << endl;
  cout << std::is_same<int, remove_const<const int>::type>::value << endl;

  //添加引用
  cout << std::is_same<int&, add_lvalue_reference<int>::type>::value << endl;
  cout << std::is_same<int&&, add_rvalue_reference<int>::type>::value << endl;

  //取公共類型
  typedef std::common_type<unsigned char, short, int>::type NumericType;
  cout << std::is_same<int, NumericType>::value << endl;

  return 0;
}

  type_traits還提供了編譯期選擇traits:std::conditional,它在編譯期根據一個判斷式選擇兩個類型中的一個,和條件表達式的語義類似,類似於一個三元表達式。它的原型是:

template< bool B, class T, class F >
struct conditional;

用法比較簡單:

#include <iostream>
#include <type_traits>

int main() 
{
    typedef std::conditional<true,int,float>::type A;               // int
    typedef std::conditional<false,int,float>::type B;              // float

    typedef std::conditional<(sizeof(long long) >sizeof(long double)),
    long long, long double>::type max_size_t;

    cout<<typeid(max_size_t).name()<<endl;  //long double
}

  另外一個常用的type_traits是std::decay(朽化),它對於普通類型來說std::decay(朽化)是移除引用和cv符,大大簡化了我們的書寫。除了普通類型之外,std::decay還可以用於數組和函數,具體的轉換規則是這樣的:

  先移除T類型的引用,得到類型U,U定義為remove_reference<T>::type。

  • 如果is_array<U>::value為 true,修改類型type為remove_extent<U>::type *。
  • 否則,如果is_function<U>::value為 true,修改類型type將為add_pointer<U>::type。
  • 否則,修改類型type為 remove_cv<U>::type。

std::decay的基本用法:

typedef std::decay<int>::type A;           // int
typedef std::decay<int&>::type B;          // int
typedef std::decay<int&&>::type C;         // int
typedef std::decay<constint&>::type D;    // int
typedef std::decay<int[2]>::type E;        // int*
typedef std::decay<int(int)>::type F;      // int(*)(int)

  std::decay除了移除普通類型的cv符的作用之外,還可以將函數類型轉換為函數指針類型,從而將函數指針變量保存起來,以便在后面延遲執行,比如下面的例子。

template<typename F>
struct SimpFunction
{
    using FnType = typename std::decay<F>::type;//先移除引用再添加指針

    SimpFunction(F& f) : m_fn(f){}

    void Run()
    {
        m_fn();
    }

    FnType m_fn;
};

  如果要保存輸入的函數,則先要獲取函數對應的函數指針類型,這時就可以用std::decay來獲取函數指針類型了,using FnType = typename std::decay<F>::type;實現函數指針類型的定義。type_traits還提供了獲取可調用對象返回類型的元函數:std::result_of,它的基本用法:

int fn(int) {return int();}                            // function
typedef int(&fn_ref)(int);                             // function reference
typedef int(*fn_ptr)(int);                             // function pointer
struct fn_class { int operator()(int i){return i;} };  // function-like class

int main() {
  typedef std::result_of<decltype(fn)&(int)>::type A;  // int
  typedef std::result_of<fn_ref(int)>::type B;         // int
  typedef std::result_of<fn_ptr(int)>::type C;         // int
  typedef std::result_of<fn_class(int)>::type D;       // int
}

  type_traits還提供了一個很有用的元函數std::enable_if,它利用SFINAE(substitude failure is not an error)特性,根據條件選擇重載函數的元函數std::enable_if,它的原型是:

template<bool B, class T = void> struct enable_if;

  根據enable_if的字面意思就可以知道,它使得函數在判斷條件B僅僅為true時才有效,它的基本用法:

template <class T>
typename std::enable_if<std::is_arithmetic<T>::value, T>::type foo(T t)
{
    return t;
}
auto r = foo(1); //返回整數1
auto r1 = foo(1.2); //返回浮點數1.2
auto r2 = foo(“test”); //compile error

  在上面的例子中對模板參數T做了限定,即只能是arithmetic(整型和浮點型)類型,如果為非arithmetic類型,則編譯不通過,因為std::enable_if只對滿足判斷式條件的函數有效,對其他函數無效。

  可以通過enable_if來實現編譯期的if-else邏輯,比如下面的例子通過enable_if和條件判斷式來將入參分為兩大類,從而滿足所有的入參類型:

template <class T>
typename std::enable_if<std::is_arithmetic<T>::value, int>::type foo1(T t)
{
    cout << t << endl;
    return 0;
}

template <class T>
typename std::enable_if<!std::is_arithmetic<T>::value, int>::type foo1(T &t)
{
    cout << typeid(T).name() << endl;
    return 1;
}

  對於arithmetic類型的入參則返回0,對於非arithmetic的類型則返回1,通過arithmetic將所有的入參類型分成了兩大類進行處理。從上面的例子還可以看到,std::enable_if可以實現強大的重載機制,因為通常必須是參數不同才能重載,如果只有返回值不同是不能重載的,而在上面的例子中,返回類型相同的函數都可以重載。

  C++11的type_traits提供了近百個在編譯期計算、查詢、判斷、轉換和選擇的元函數,為我們編寫元程序提供了很大的便利。如果說C++11的type_traits讓模版元編程變得簡單,那么C++11提供的可變模板參數和tuple則進一步增強了模板元編程。

4.可變模板參數

  C++11的可變模版參數(variadic templates)是C++11新增的最強大的特性之一,它對參數進行了高度泛化,它能表示0到任意個數、任意類型的參數。關於它的用法和使用技巧讀者可以參考筆者在程序員2015年2月A上的文章:泛化之美--C++11可變模版參數的妙用,這里不再贅述,這里將要展示的如何借助可變模板參數實現一些編譯期算法,比如獲取最大值、判斷是否包含了某個類型、根據索引查找類型、獲取類型的索引和遍歷類型等算法。實現這些算法需要結合type_traits或其它C++11特性,下面來看看這些編譯期算法是如何實現的。

  編譯期從一個整形序列中獲取最大值:

//獲取最大的整數
template <size_t arg, size_t... rest>
struct IntegerMax;

template <size_t arg>
struct IntegerMax<arg> : std::integral_constant<size_t, arg>
{
};

template <size_t arg1, size_t arg2, size_t... rest>
struct IntegerMax<arg1, arg2, rest...> : std::integral_constant<size_t, arg1 >= arg2 ? IntegerMax<arg1, rest...>::value :
    IntegerMax<arg2, rest...>::value >
{
};

  這個IntegerMax的實現用到了type_traits中的std::integral_const,它在展開參數包的過程中,不斷的比較,直到所有的參數都比較完,最終std::integral_const的value值即為最大值。它的使用很簡單:

cout << IntegerMax<2, 5, 1, 7, 3>::value << endl; //value為7

  我們可以在IntegerMax的基礎上輕松的實現獲取最大內存對齊值的元函數MaxAlign。

  編譯期獲取最大的align:

template<typename... Args>
struct MaxAlign : std::integral_constant<int, IntegerMax<std::alignment_of<Args>::value...>::value>{};
cout << MaxAlign<int, short, double, char>::value << endl; //value為8
    編譯判斷是否包含了某種類型:
template < typename T, typename... List >
struct Contains;

template < typename T, typename Head, typename... Rest >
struct Contains<T, Head, Rest...>
    : std::conditional< std::is_same<T, Head>::value, std::true_type, Contains<T, Rest... >> ::type{};

template < typename T >
struct Contains<T> : std::false_type{};
用法:cout<<Contains<int, char, double, int, short>::value<<endl; //輸出true

  這個Contains的實現用到了type_traits的std::conditional、std::is_same、std::true_type和std::false_type,它的實現思路是在展開參數包的過程中不斷的比較類型是否相同,如果相同則設置值為true,否則設置為false。

        編譯期獲取類型的索引:

template < typename T, typename... List >
struct IndexOf;

template < typename T, typename Head, typename... Rest >
struct IndexOf<T, Head, Rest...>
{
    enum{ value = IndexOf<T, Rest...>::value+1 };
};

template < typename T, typename... Rest >
struct IndexOf<T, T, Rest...>
{
    enum{ value = 0 };
};

template < typename T >
struct IndexOf<T>
{
    enum{value = -1};
};

  用法:cout<< IndexOf<int, double, short, char, int, float>::value<<endl; //輸出3

  這個IndexOf的實現比較簡單,在展開參數包的過程中看是否匹配到特化的IndexOf<T, T, Rest...>,如果匹配上則終止遞歸將之前的value累加起來得到目標類型的索引位置,否則將value加1,如果所有的類型中都沒有對應的類型則返回-1;

  編譯期根據索引位置查找類型:

template<int index, typename... Types>
struct At;

template<int index, typename First, typename... Types>
struct At<index, First, Types...>
{
    using type = typename At<index - 1, Types...>::type;
};

template<typename T, typename... Types>
struct At<0, T, Types...>
{
    using type = T;
};
    用法:
using T = At<1, int, double, char>::type;
    cout << typeid(T).name() << endl; //輸出double

  At的實現比較簡單,只要在展開參數包的過程中,不斷的將索引遞減至0時為止即可獲取對應索引位置的類型。接下來看看如何在編譯期遍歷類型。

template<typename T>
void printarg()
{
    cout << typeid(T).name() << endl;
}

template<typename... Args>
void for_each() 
{
    std::initializer_list<int>{(printarg<Args>(), 0)...};
}
用法:for_each<int,double>();//將輸出int double

  這里for_each的實現是通過初始化列表和逗號表達式來遍歷可變模板參數的。

  可以看到,借助可變模板參數和type_traits以及模板偏特化和遞歸等方式我們可以實現一些有用的編譯期算法,這些算法為我們編寫應用層級別的代碼奠定了基礎,后面模板元編程的具體應用中將會用到這些元函數。

  C++11提供的tuple讓我們編寫模版元程序變得更靈活了,在一定程度上增強了C++的泛型編程能力,下面來看看tuple如何應用於元程序中的。

5.tuple與模版元

  C++11的tuple本身就是一個可變模板參數組成的元函數,它的原型如下:

template<class...Types>
class tuple;

  tuple在模版元編程中的一個應用場景是將可變模板參數保存起來,因為可變模板參數不能直接作為變量保存起來,需要借助tuple保存起來,保存之后再在需要的時候通過一些手段將tuple又轉換為可變模板參數,這個過程有點類似於化學中的“氧化還原反應”。看看下面的例子中,可變模板參數和tuple是如何相互轉換的:

//定義整形序列
template<int...>
struct IndexSeq{};

//生成整形序列
template<int N, int... Indexes>
struct MakeIndexes : MakeIndexes<N - 1, N - 1, Indexes...>{};

template<int... indexes>
struct MakeIndexes<0, indexes...>{
    typedef IndexSeq<indexes...> type;
};

template<typename... Args>
void printargs(Args... args){
    //先將可變模板參數保存到tuple中
    print_helper(typename MakeIndexes<sizeof... (Args)>::type(), std::make_tuple(args...));
}

template<int... Indexes, typename... Args>
void print_helper(IndexSeq<Indexes...>, std::tuple<Args...>&& tup){
    //再將tuple轉換為可變模板參數,將參數還原回來,再調用print
    print(std::get<Indexes>(tup)...); 
}
template<typename T>
void print(T t)
{
    cout << t << endl;
}

template<typename T, typename... Args>
void print(T t, Args... args)
{
    print(t);
    print(args...);
}

  用法:printargs(1, 2.5, “test”); //將輸出1 2.5 test

  上面的例子print實際上是輸出可變模板參數的內容,具體做法是先將可變模板參數保存到tuple中,然后再通過元函數MakeIndexes生成一個整形序列,這個整形序列就是IndexSeq<0,1,2>,整形序列代表了tuple中元素的索引,生成整形序列之后再調用print_helper,在print_helper中展開這個整形序列,展開的過程中根據具體的索引從tuple中獲取對應的元素,最終將從tuple中取出來的元素組成一個可變模板參數,從而實現了tuple“還原”為可變模板參數,最終調用print打印可變模板參數。

  tuple在模板元編程中的另外一個應用場景是用來實現一些編譯期算法,比如常見的遍歷、查找和合並等算法,實現的思路和可變模板參數實現的編譯期算法類似,關於tuple相關的算法,讀者可以參考筆者在github上的代碼:https://github.com/qicosmos/cosmos/tree/master/tuple

  下面來看看模版元的具體應用。

6.模版元的應用

  我們將展示如何通過模版元來實現function_traits和Vairant類型。

  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)>::function_type>();//將輸出int __cdecl(int)
}

  有了這個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
View Code

  通過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
};
View Code

  實現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中存儲的對象的實際類型。

7總結

  C++11中的一些特性比如type_traits、可變模板參數和tuple讓模版元編程變得更簡單也更強大,模版元編程雖然功能強大,但也比較復雜,要用好模版元,需要我們轉變思維方式,在掌握基本的理論的基礎上,再認真揣摩模版元的一些常用技巧,這些技巧是有規律可循的,基本上都是通過重定義、遞歸和偏特化等手法來實現的,當我們對這些基本技巧很熟悉的時候再結合不斷地實踐,相信對模版元編程就能做到“游刃有余”了。

 

本文曾發表於《程序員》2015年3月刊。轉載請注明出處。

后記:本文的內容主要來自於我在公司內部培訓的一次課程,因為很多人對C++11模板元編程理解得不深入,所以我覺得有必要拿出來分享一下,讓更多的人看到,就整理了一下發到程序員雜志了,我相信讀者看完之后對模版元會有全面深入的了解。


免責聲明!

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



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