C++11新增了模版元相关的特性,不仅让我们编写模版元程序变得更容易,还进一步增强了泛型编程能力,本文将向读者展示C++11中模版元编程的常用技巧。
概述
模版元编程(Template Metaprogram)是C++中最复杂也是威力最强大的编程范式,它是一种可以创建和操纵程序的程序。模版元编程完全不同于普通的运行时程序,它很独特,因为其执行完全在编译期,操纵的数据不能是运行时变量,只能是编译期常量,不可修改,另外语法元素也相当有限,不能使用运行时的某些语法,如if-else、for等语句。因此,运用模版元编程需要颇多技巧,常需要类型重定义、枚举常量、继承、模板偏特化等方法与之配合。
模版元基本概念
模版元程序由元数据和元函数组成,前者就是元编程可操作的数据,即C++编译器在编译期可操作的数据。元数据不是运行时变量,只能是编译期常量,不能修改,常见的元数据有enum枚举常量、静态常量、基本类型和自定义类型等。
元函数是模板元编程中用于操作处理元数据的“构件”,可以在编译期被“调用”,因为它的功能和形式与运行时函数类似,因而被称为元函数,它是元编程中最重要的构件。元函数实际上表现为C++的一个类、模板类或模板函数,它的通常形式如下:
template< INT M int N,> struct meta_func { static const value = N+M; }
调用元函数获取value值:
cout<< META_FUNC <1, 2>::value<< ENDL ;< pre> < SCRIPT type = text /javascript> function path() { var args = arguments, result = [] ; for(var i = 0; i < args.length ; i++) result.push(args[i].replace('@', '/cms/js/syntax/scripts/')); return result }; SyntaxHighlighter.autoloader.apply(null, path( 'applescript @shBrushAppleScript.js', 'actionscript3 as3 @shBrushAS3.js', 'bash shell @shBrushBash.js', 'coldfusion cf @shBrushColdFusion.js', 'cpp c @shBrushCpp.js', 'c# c-sharp csharp @shBrushCSharp.js', 'css @shBrushCss.js', 'delphi pascal @shBrushDelphi.js', 'diff patch pas @shBrushDiff.js', 'erl erlang @shBrushErlang.js', 'groovy @shBrushGroovy.js', 'java @shBrushJava.js', 'jfx javafx @shBrushJavaFX.js', 'js jscript javascript @shBrushJScript.js', 'perl pl @shBrushPerl.js', 'php @shBrushPhp.js', 'text plain @shBrushPlain.js', 'py python @shBrushPython.js', 'ruby rails ror rb @shBrushRuby.js', 'sass scss @shBrushSass.js', 'scala @shBrushScala.js', 'sql @shBrushSql.js', 'vb vbnet @shBrushVb.js', 'xml xhtml xslt html @shBrushXml.js' )); SyntaxHighlighter.all();
meta_func的执行过程在编译期完成,实际执行程序时,没有计算动作,而是直接使用编译期的计算结果。
元函数只处理元数据,元数据是编译期常量和类型,所以下面的代码是编译不过的:
2 int i = 1, j = 2; meta_func< I , j>::value; //错误,元函数无法处理运行时普通数据
模板元编程产生的源程序是在编译期执行的程序,首先要遵循C++和模板语法,但它操作的对象不是运行时普通的变量,因此不能使用运行时关键字,其可用语法元素相当有限,最常用的有下面几种:
1. enum、static const:用来定义编译期的整数常量;
2. typedef/using:用于定义元数据;
3. T、Args...:声明元数据类型;
4. Template:主要用于定义元函数;
5. "::":域运算符,用于解析类型作用域获取计算结果(元数据)。
如果模板元编程中需要if-else、for等逻辑时该怎么办呢?
if-else可通过type_traits实现,它不仅可在编译期做判断,还可做计算、查询、转换和选择。
模板元中的for等逻辑可通过递归、重载、模板特化(偏特化)等方法实现。
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简单,之前定义编译期常量一般要这样:
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提供了上百个方便的元函数,读者可以参考http://en.cppreference.com/w/cpp/header/type_traits,这些基本的元函数用法比较简单,如代码9所示。
type_traits还提供了编译期选择traits:std::conditional,它在编译期根据一个判断式选择两个类型中的一个,与条件表达式的语义相近,类似三元表达式。它的原型如代码10所示。
用法也比较简单,如代码11所示。
#include < IOSTREAM > #include < TYPE_TRAITS > int main() { std::cout << "int: " << std::is_const ::value << std::endl ; std::cout << "const int: " << std::is_const::value << std::endl ; //判断类型是否相同 std::cout<< std::is_same::value<<"/n";// true std::cout<< std::is_same ::value<<"/n";// false //添加、移除const cout << std::is_same ::type>::value << endl ; cout << std::is_same::type>::value << endl ; //添加引用 cout << std::is_same::type>::value << endl ; cout << std::is_same::type>::value << endl ; //取公共类型 typedef std::common_type::type NumericType; cout << std::is_same ::value << endl; return 0; }
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()< < SCRIPT type = text /javascript> function path() { var args = arguments, result = [] ; for(var i = 0; i < args.length ; i++) result.push(args[i].replace('@', '/cms/js/syntax/scripts/')); return result }; SyntaxHighlighter.autoloader.apply(null, path( 'applescript @shBrushAppleScript.js', 'actionscript3 as3 @shBrushAS3.js', 'bash shell @shBrushBash.js', 'coldfusion cf @shBrushColdFusion.js', 'cpp c @shBrushCpp.js', 'c# c-sharp csharp @shBrushCSharp.js', 'css @shBrushCss.js', 'delphi pascal @shBrushDelphi.js', 'diff patch pas @shBrushDiff.js', 'erl erlang @shBrushErlang.js', 'groovy @shBrushGroovy.js', 'java @shBrushJava.js', 'jfx javafx @shBrushJavaFX.js', 'js jscript javascript @shBrushJScript.js', 'perl pl @shBrushPerl.js', 'php @shBrushPhp.js', 'text plain @shBrushPlain.js', 'py python @shBrushPython.js', 'ruby rails ror rb @shBrushRuby.js', 'sass scss @shBrushSass.js', 'scala @shBrushScala.js', 'sql @shBrushSql.js', 'vb vbnet @shBrushVb.js', 'xml xhtml xslt html @shBrushXml.js' )); SyntaxHighlighter.all();
另一个常用的type_traits是std::decay(朽化),对普通类型来说std::decay是移除引用和cv符,大大简化了书写。除了普通类型,它还可用于数组和函数,具体转换规则如下。
先移除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; };
如果要保存输入函数,则先要获取函数对应的指针类型,这时就可用using FnType = typename std::decay::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 struct enable_if;
根据enable_if的字面意思就可知道,它使得函数在判断条件B仅为true时才有效,其基本用法为:
template < CLASS T> typename std::enable_if< STD::IS_ARITHMETIC ::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(整型和浮点型)类型,若非如此,则编译不通过,因为std::enable_if只对满足判断式条件的函数有效。
可通过enable_if实现编译期的if-else逻辑,下面的例子通过enable_if和条件判断式将入参分为两大类,从而满足所有的入参类型:
template < CLASS T> typename std::enable_if< STD::IS_ARITHMETIC ::value, int>::type foo1(T t) { cout <&l t ; t << endl; return 0; } template typename std::enable_if T >::value, int>::type foo1(T &t) { cout << typeid(T).name() << endl; return 1; }
对于arithmetic类型的入参返回0,对于非arithmetic的类型则返回1。从上面的例子还可以看到,std::enable_if可实现强大的重载机制,因为通常必须参数不同才能重载,如果只有返回值不同是不行的,而在上面的例子中,返回类型相同的函数都可重载。
C++11的type_traits提供了近百个在编译期计算、查询、判断、转换和选择的元函数,为我们编写元程序提供了便利。如果说C++11的type_traits让模版元编程变得简单,那么可变模板参数和tuple则进一步增强了模板元编程。
可变模板参数
可变模版参数(Variadic Templates)是C++11新增的强大特性之一,它对参数进行了高度泛化,能表示0到任意个数、任意类型的参数。关于它的用法和使用技巧读者可参考2015年2月A中的文章《泛化之美--C++11可变模版参数的妙用》,不再赘述。这里将展示如何借助可变模板参数实现一些编译期算法,如获取最大值、判断是否包含了某个类型、根据索引查找类型、获取类型索引和遍历类型等算法。实现这些算法需要结合type_traits或其它C++11特性。
编译期从一个整形序列中获取最大值:
//获取最大的整数 template < SIZE_T rest size_t... arg,> struct IntegerMax; template < SIZE_T arg> struct IntegerMax< ARG > : std::integral_constant< SIZE_T , arg> { }; template < SIZE_T rest size_t... arg2, size_t arg1,> 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
struct MaxAlign : std::integral_constant< INT , IntegerMax::value...>::value>{}; cout << MaxAlign ::value << endl; //value为8
编译判断是否包含某种类型:
template < typename T, typename... List > struct Contains; template < typename T, typename Head, typename... Rest > struct Contains<T, Rest... Head,> : 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<::value<
这个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, Rest... Head,> { enum{ value = IndexOf<T, Rest...>::value+1 }; }; template < typename T, typename... Rest > struct IndexOf<T, Rest... T,> { enum{ value = 0 }; }; template < typename T > struct IndexOf<T> { enum{value = -1}; };
(用法:cout<< IndexOf::value<
这个IndexOf的实现比较简单,在展开参数包的过程中看是否匹配特化的IndexOf,若匹配,则终止递归,将之前的value累加起来得到目标类型的索引位置,否则将value加1,如果所有的类型中都没有对应的类型则返回-1。
编译期根据索引位置查找类型:
template<INT Types typename... index,> struct At; template<INT Types typename... index, First, typename> struct At<INDEX, First, Types...> { using type = typename At<INDEX Types... 1, ->::type; }; template<TYPENAME Types typename... T,> 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。)
这里for_each的实现是通过初始化列表和逗号表达式来遍历可变模板参数的。
可以看到,借助可变模板参数和type_traits,以及模板偏特化和递归等方式我们可实现一些有用的编译期算法,这些算法为我们编写应用层级别的代码奠定了基础。
C++11提供的tuple让编写模版元程序变得更灵活,在一定程度上增强了C++的泛型编程能力,下面来看tuple如何应用于元程序中的。
tuple与模版元
C++11的tuple本身就是一个可变模板参数组成的元函数,它的原型如下:
template<CLASS...TYPES> class tuple;
tuple在模版元编程中的一个应用场景是将可变模板参数保存,因为可变模板参数不能直接作为变量保存,之后再在需要时通过一些手段将tuple转换为可变模板参数,这个过程有点类似于化学中的“氧化还原反应”。且看下面的例子,可变模板参数和tuple是如何相互转换的:
//定义整形序列 template< INT... > struct IndexSeq{}; //生成整形序列 template< INT Indexes int... N,> struct MakeIndexes : MakeIndexes< N Indexes... 1, - N>{}; 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... Args typename... Indexes,> void print_helper(IndexSeq< INDEXES... >, std::tuple< ARGS... >&& tup){ //再将tuple转换为可变模板参数,将参数还原回来,再调用print print(std::get< INDEXES >(tup)...); } template< TYPENAME T> void print(T t) { cout <&l t ; t << endl; } template 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,在其中展开这个整形序列,展开的过程根据具体的索引从tuple中获取对应的元素,最终将从tuple中取出的元素组成一个可变模板参数,从而实现了tuple“还原”为可变模板参数,最终调用print打印。
tuple在模板元编程中的另外一个应用场景是实现编译期算法,比如常见的遍历、查找和合并等,实现思路与可变模板参数实现的编译期算法类似,关于tuple相关的算法,读者可以参考我在GitHub上的代码https://github.com/qicosmos/cosmos/tree/master/tuple。
tuple在模版元编程中还有一个应用,它可充当编译期和运行期结合的“桥梁”,因为模版元能操作的数据只能是编译期常量,无法操作运行时常量,如果希望运行时变量转换为编译期常量该如何实现呢?虽不能直接转换,但可通过enable_if和tuple间接转换,请看下面的例子:
template< SIZE_T Tuple typename k,> typename std::enable_if < (k == std::tuple_size< TUPLE >::value)>::type PrintArgByIndex(const Tuple& tp, size_t index) { throw std::invalid_argument("arg index out of range"); } template< SIZE_T Tuple typename k = "0," > typename std::enable_if < (k < std::tuple_size ::value)>::type PrintArgByIndex(const Tuple& tp, size_t index) { if (k == index) { cout << std::get (tp) << endl ; } else { PrintArgByIndex(tp, index); } } int _main() { std::tuple< INT , string double,> tp = std::make_tuple(1, 2.5, "aa"); int index = 1; PrintArgByIndex(tp, index); //输出2.5 index = 2; PrintArgByIndex(tp, index); //输出aa }
可以看到输入是运行时变量,而不是编译期变量,我们最终获取了tuple中的元素(必须通过编译期常量)并打印出来,正是tuple和enable_if架起了编译期和运行时的桥梁,让我们在递归调用PrintArgByIndex时不断递增索引,并将编译期常量和运行时变量比较,二者相等时则从tuple中取出对应的元素。这种方法为我们将运行时变量运用于编译期算法提供了思路,增强了模版元编程的灵活性。
总结
C++11中的type_traits、可变模板参数和tuple让模版元编程变得更简单也更强大,但也比较复杂,这种复杂性源自它们正是用于解决复杂的问题。模版元程序最终给用户提供的接口却简单易用,这也是模版元编程的一个特点——隐藏复杂性,解决复杂的问题,给用户提供最简单的接口。这也体现了模版元编程之美。
作者:祁宇
作者简介:金山WPS资深工程师,负责Android服务端开发。爱好开源,主要研究方向为架构设计和业务重构。个人博客 www.cnblogs.com/qicosmos