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