C++模板元编程简介
1.概述
模板元编程(Template Metaprogramming,TMP)是编写⽣成或操纵程序的程序,也是⼀种复杂且功能强⼤的编程范式(Programming Paradigm)。C++模板给C++提供了元编程的能⼒,但⼤部分⽤户对C++模板的使⽤并不是很频繁,⼤致限于泛型编程,在⼀些系统级的代码,尤其是对通⽤性、性能要求极⾼的基础库(如 STL、Boost)⼏乎不可避免在⼤量地使⽤ C++ 模板以及模板元编程。
模版元编程完全不同于普通的运⾏期程序,因为模版元程序的执⾏完全是在编译期,并且模版元程序操纵的数据不能是运⾏时变量,只能是编译期常量,不可修改。另外它⽤到的语法元素也是相当有限,不能使⽤运⾏期的⼀些语法,⽐如if-else、for和while等语句都不能⽤。因此,模版元编程需要很多技巧,常常需要类型重定义、枚举常量、继承、模板偏特化等⽅法来配合,因此模版元编程⽐较复杂也⽐较困难。
2.模板元编程的作⽤
C++ 模板最初是为实现泛型编程设计的,但⼈们发现模板的能⼒远远不⽌于那些设计的功能。⼀个重要的理论结论就是:C++ 模板是图灵完备的(Turing-complete),就是⽤ C++ 模板可以模拟图灵机。理论上说 C++ 模板可以执⾏任何计算任务,但实际上因为模板是编译期计算,其能⼒受到具体编译器实现的限制(如递归嵌套深度,C++11要求⾄少1024层,C++98要求⾄少17层)。C++ 模板元编程
是“意外”功能,⽽不是设计的功能,这也是 C++ 模板元编程语法丑陋的根源。
C++ 模板是图灵完备的,这使得 C++代码存在两层次,其中,执⾏编译计算的代码称为静态代码(static code),执⾏运⾏期计算的代码称为动态代码(dynamic code),C++的静态代码由模板实现,编写C++的静态代码,就是进⾏C++的模板元编程。
具体来说 C++ 模板可以做以下事情:编译期数值计算、类型计算、代码计算(如循环展开),其中数值计算实际意义不⼤,⽽类型计算和代码计算可以使得代码更加通⽤,更加易⽤,性能更好(也更难阅读,更难调试,有时也会有代码膨胀问题)。编译期计算在编译过程中的位置请见下图。
使⽤模板元编程的基本原则就是:将负载由运⾏时转移到编译时,同时保持原有的抽象层次。其中负载可以分为两类,⼀类就是程序运⾏本⾝的开销,⼀类则是程序员需要编写的代码。前者可以理解为编译时优化,后者则是为提⾼代码复⽤度,从⽽提⾼程序员的编程效率。
3.模板元编程的组成要素
从编程范式上来说,C++模板元编程是函数式编程,⽤递归形式实现循环结构的功能,⽤C++ 模板的特例化提供了条件判断能⼒,这两点使得其具有和普通语⾔⼀样通⽤的能⼒(图灵完备性)。
模版元程序由元数据和元函数组成,元数据就是元编程可以操作的数据,即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 //⽤于定义元数据;
//声明元数据类型;
Template //主要⽤于定义元函数;
:: //域运算符,⽤于解析类型作⽤域获取计算结果(元数据)。
实际上,模板元中的if-else可以通过type_traits来实现,它不仅仅可以在编译期做判断,还可以做计算、查询、转换和选择。模板元中的for等逻辑可以通过递归、重载、和模板特化(偏特化)等⽅法实现。
4.模板元编程的控制逻辑
第⼀个 C++ 模板元程序由Erwin Unruh 在 1994 年编写,这个程序计算⼩于给定数 N 的全部素数(⼜叫质数),程序并不运⾏(都不能通过编译),⽽是让编译器在错误信息中显⽰结果(直观展现了编译期计算结果),C++ 模板元编程不是设计的功能,更像是在戏弄编译器。从此,C++模板元编程的能⼒开始被⼈们认识到。
在模版元程序的具体实现时,由于其执⾏完全是在编译期,所以不能使⽤运⾏期的⼀些语法,⽐如if-else、for和while等语句都不能⽤。这些控制逻辑需要通过特殊的⽅法来实现。
4.1 if判断
模板元编程中实现条件if判断,参考如下代码:
#include <iostream>
template<bool c, typename Then, typename Else> class IF_ {};
template<typename Then, typename Else>
class IF_<true, Then, Else> { public: typedef Then reType; };
template<typename Then, typename Else>
class IF_<false,Then, Else> { public: typedef Else reType; };
int main()
{
const int len = 4;
// 定义⼀个指定字节数的类型
typedef
IF_<sizeof(short)==len, short,
IF_<sizeof(int)==len, int,
IF_<sizeof(long)==len, long,
IF_<sizeof(long long)==len, long long,
void>::reType>::reType>::reType>::reType int_my;
std::cout << sizeof(int_my) << '\n';
}
实际上,从C++11开始,可以通过type_traits来实现模板元编程中的if判断。因为type_traits提供了编译期选择特性:std::conditional,它在编译期根据⼀个判断式选择两个类型中的⼀个,和条件表达式的语义类似,类似于⼀个三元表达式。它的原型是:
template<bool B, class T, class F>
struct conditional;
所以上⾯的代码可以改写为如下代码:
#include <iostream>
#include <type_traits>
int main()
{
const int len = 4;
// 定义⼀个指定字节数的类型
typedef
std::conditional<sizeof(short)==len, short,
std::conditional<sizeof(int)==len, int,
std::conditional<sizeof(long)==len, long,
std::conditional<sizeof(long long)==len, long long,
void>::type>::type>::type>::type int_my;
std::cout << sizeof(int_my) << '\n';
}
程序同样编译输出4。
4.2循环展开
编译期的循环展开( Loop Unrolling)可以通过模板特化来结束递归展开,达到运⾏期的for和while语句的功能。下⾯看⼀个编译期数值计算的例⼦。
#include <iostream>
template<int N> class sum
{
public: static const int ret = sum<N-1>::ret + N;
};
template<> class sum<0>
{
public: static const int ret = 0;
};
int main()
{
std::cout << sum<5>::ret <<std::endl;
return 0;
}
当编译器遇到sumt<5>时,试图实例化之,sumt<5> 引⽤了sumt<5-1>即sumt<4>,试图实例化sumt<4>,以此类推,直到
sumt<0>,sumt<0>匹配模板特例,sumt<0>::ret为 0,sumt<1>::ret为sumt<0>::ret+1为1,以此类推,sumt<5>::ret为15。值得⼀提的是,虽然对⽤户来说程序只是输出了⼀个编译期常量sumt<5>::ret,但在
背后,编译器其实⾄少处理了sumt<0>到sumt<5>共6个类型。
从这个例⼦我们也可以窥探 C++ 模板元编程的函数式编程范型,对⽐结构化求和程序:for(i=0,sum=0; i<=N; ++i) sum+=i;⽤逐步改变存储(即变量 sum)的⽅式来对计算过程进⾏编程,模板元程序没有可变的存储(都是编译期常量,是不可变的变量),要表达求和过程就要⽤很多个常量:sumt<0>::ret,sumt<1>::ret,...,sumt<5>::ret。函数式编程看上去似乎效率低下(因为它和数学接近,⽽不是和硬件⼯作⽅式接近),但有⾃⼰的优势:描述问题更加简洁清晰,没有可变的变量就没有数据依赖,⽅便进⾏并⾏化。
4.3switch/case分⽀
同样可以通过模板特化来模拟实现编译期的switch/case分⽀功能。参考如下代码:
#include <iostream>
using namespace std;
template<int v> class Case
{
public:
static inline void Run()
{
cout << "default case" << endl;
}
};
template<> class Case<1>
{
public:
static inline void Run()
{
cout << "case 1" << endl;
}
};
template<> class Case<2>
{
public:
static inline void Run()
{
cout << "case 2" << endl;
}
};
int main()
{
Case<2>::Run();
}
程序输出结果:
case 2
1
5.特性、策略与标签
利⽤迭代器,我们可以实现很多通⽤算法,迭代器在容器与算法之间搭建了⼀座桥梁。求和函数模板如下:
#include <iostream>
#include <vector>
template<typename iter>
typename iter::value_type mysum(iter begin, iter end)
{
typename iter::value_type sum(0);
for(iter i=begin; i!=end; ++i)
sum += *i;
return sum;
}
int main()
{
std::vector<int> v;
for(int i = 0; i<100; ++i)
v.push_back(i);v.push_back(i);
std::cout << mysum(v.begin(), v.end()) << '\n';
}
程序编译输出:4950。
我们想让 mysum() 对指针参数也能⼯作,毕竟迭代器就是模拟指针,但指针没有嵌套类型 value_type,可以定义 mysum() 对指针类型的特例,但更好的办法是在函数参数和 value_type 之间多加⼀层特性(traits)。
template<typename iter>
class mytraits
{
public: typedef typename iter::value_type value_type;
};
template<typename T>
class mytraits<T*>
{
access常见条件表达式public: typedef T value_type;
};
template<typename iter>
typename mytraits<iter>::value_type mysum(iter begin, iter end)
{
typename mytraits<iter>::value_type sum(0);
for(iter i=begin; i!=end; ++i)
sum += *i;sum += *i;
return sum;
}
int main()
{
int v[4] = {1,2,3,4};
std::cout << mysum(v, v+4) << '\n';
return 0;
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论