1. 程式人生 > >C++11模版超程式設計

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模板超程式設計理解得不深入,所以我覺得有必要拿出來分享一下,讓更多的人看到,就整理了一下發到程式設計師雜誌了,我相信讀者看完之後對模版元會有全面深入的瞭解。

相關推薦

C++11模版程式設計

1.概述   模版超程式設計(template metaprogram)是C++中最複雜也是威力最強大的程式設計正規化,它是一種可以建立和操縱程式的程式。模版超程式設計完全不同於普通的執行期程式,它很獨特,因為模版元程式的執行完全是在編譯期,並且模版元程式操縱的資料不能是執行時變數,只能是編譯期常量,不可修

C++之模板程式設計

關於模板原程式設計知識強烈推薦:http://blog.jobbole.com/83461/,非常好! 這篇文章通過舉例詳細介紹了模板的模板引數,模板特例化,模板例項化以及編譯連結等模板基礎知識。 本文主要分析文章中的模板超程式設計例子: 首先複述一下模板超程式設計,以下標紅或者

C++98 到 C++17,程式設計是如何演進的? | 技術頭條

作者 | 祁宇責編 | 郭芮出品 | CSDN(ID:CSDNnews) 不斷出現的C++新的標準,正在改變超程式設計的程式設計思想,新的idea和方法不斷湧現,讓超程式設計變得越來越簡單,讓C++變得簡單也是C++未來的一個趨勢。 很多人對超程式設計有一些誤解,認為

現代c++與模板程式設計

最近在重溫《c++程式設計新思維》這本經典著作,感慨頗多。由於成書較早,書中很多超程式設計的例子使用c++98實現的。而如今c++20即將帶著concept,Ranges等新特性一同到來,不得不說光陰荏苒。在c++11之後,得益於新標準很多超程式設計的複雜技巧能被簡化了,STL也提供了諸如<type_t

C/C++程式設計教訓----函式內靜態類物件初始化非執行緒安全(C++11之前)

不少程式設計師在編寫程式的時候,會使用函式內靜態(static)變數,既能滿足函式內這個變數可以持久的記錄某些資訊,又使其訪問範圍的控制侷限於函式內。但函式內靜態類物件初始化是非執行緒安全的。 問題背景 在我們產品中對log4cxx做了一些簡單的封裝 (採用VS2005編譯),其中會

C++11 thread程式設計呼叫類方法並傳入引數

#include <thread> #include <iostream> class classA { public : classA() { std::cout<<" classA " <<std::endl; }

c++模板程式設計:std::invoke原始碼分析及其實現

在實現invoke之前,我們先看一下標準庫種invoke的使用方式 template< class F, class... Args>std::invoke_result_t<F, Args...> invoke(F&& f, Args&&... ar

程式設計技巧與C++11特性】總結

一,程式設計技巧 1.1排序效能問題 C ++的排序函式有兩種用法: 傳入一個functor物件; 直接傳入一個排序函式。 #include <iostream> #include <ctime> #include <algorithm&

C++11併發程式設計:原子操作atomic

一:概述   專案中經常用遇到多執行緒操作共享資料問題,常用的處理方式是對共享資料進行加鎖,如果多執行緒操作共享變數也同樣採用這種方式。   為什麼要對共享變數加鎖或使用原子操作?如兩個執行緒操作同一變數過程中,一個執行緒執行過程中可能被核心臨時掛起,這就是執行緒切換,當核心再次切換到該執行緒時,之前的資

C++之C++ primer plus 第五單程式設計題第七題

題如下: 設計一個名為car的結構,儲存生產商(string字串或char陣列),生產年份(int)。要求:向用戶詢問有多少輛汽車i,new一下i個car結構組成的動態陣列。接著程式提示使用者輸入每輛車的生產商和年份資訊。執行結果如下: How many cars do

併發程式設計(三): 使用C++11實現無鎖stack(lock-free stack)

C++11中CAS實現: template< class T> struct atomic { public: bool compare_exchange_weak( T& expected, T desired,                    

C++11多執行緒程式設計 緒論及總結

C++11多執行緒程式設計 這一系列文章是從 https://thispointer.com/c11-multithreading-tutorial-series/ 轉過來的, 本來想翻譯一下, 但看了些內容, 用詞都不難, 讀英文沒有太大難度, 翻譯過來反而怕用詞不準畫蛇添

C++11多執行緒程式設計 第十章: 使用packaged_task優雅的讓同步函式非同步執行

C++11 Multithreading – Part 10: packaged_task<> Example and Tutorial Varun July 2, 2017 C++11 Multithreading – Part 10: packaged_tas

C++11多執行緒程式設計 第九章: std::async 更更優雅的寫多執行緒

C++11 Multithreading – Part 9: std::async Tutorial & Example Varun May 5, 2017 C++11 Multithreading – Part 9: std::async Tutorial &

C++11多執行緒程式設計 第八章: 使用 std::future std::promise 更優雅的獲取執行緒返回值

C++11 Multithreading – Part 8: std::future , std::promise and Returning values from Thread Varun June 20, 2015 C++11 Multithreading – Part

C++11多執行緒程式設計 第七章: 條件變數及其使用方法

C++11 Multithreading – Part 7: Condition Variables Explained Varun June 2, 2015 C++11 Multithreading – Part 7: Condition Variables Explain

C++11多執行緒程式設計 第五章: 使用鎖來解決竟態條件

C++11 Multithreading – Part 5: Using mutex to fix Race Conditions Varun February 22, 2015 C++11 Multithreading – Part 5: Using mutex to fi

C++11多執行緒程式設計 第四章: 共享資料和競態條件

C++11 Multithreading – Part 4: Data Sharing and Race Conditions Varun February 21, 2015C++11 Multithreading – Part 4: Data Sharing and Race Con

C++11多執行緒程式設計 第三章: 如何向執行緒傳參

C++11 Multithreading – Part 3: Carefully Pass Arguments to Threads Varun January 22, 2015 C++11 Multithreading – Part 3: Carefully Pass Ar

C++11多執行緒程式設計 第二章: join 和 detach 執行緒

  C++11 Multithreading – Part 2: Joining and Detaching Threads Varun January 21, 2015 C++11 Multithreading – Part 2: Joining and De