转载

C++11新增了模版元相关的特性,不仅让我们编写模版元程序变得更容易,还进一步增强了泛型编程能力...

C++11模版元编程

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还提供了丰富的元函数,例如用于编译期判断的元函数有:

C++11新增了模版元相关的特性,不仅让我们编写模版元程序变得更容易,还进一步增强了泛型编程能力...

这里只列举了一小部分,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

正文到此结束
Loading...