编译期计算
模板的另外一种重要用途——编译期计算,也称作“模板元编程”。
理论上使用c++模板,可以在编译期完成任何的计算任务,现实中绝对不会有人会这么做,针对编译期的编程基本没人看的懂,但即使这样我们依旧还是要了解一下模板元编程的基本概念:它仍然有一些实用的场景,并且在实际的工程中你也可能会遇到这样的代码。
编译期计算基本就是借助模板参数递归计算来得到结果
//先定义模板函数
template <int n>
struct factorial {
//可以用静态常量或者enum hack定义编译期计算变量value
static const int value =
n * factorial<n - 1>::value;
};
//在特化 n = 0时的情况
template <>
struct factorial<0> {
static const int value = 1;
};
上面定义了一个递归形式的 阶乘函数 (0!=1,n!=nx(n-1)!)
直接看编译输出。下面直接贴出对上面这样的代码加输出(printf(“%d\n”, factorial<10>::value);)在 x86-64 下的编译结果:
.LC0:
.string “%d\n”
main:
push rbp
mov rbp, rsp
mov esi, 3628800 //编译结果里明明白白直接出现了常量 10!=3628800。上面那些递归什么的,完全都没有了踪影
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
mov eax, 0
pop rbp
ret
如果我们传递一个负数给 factorial 呢?这时的结果就应该是编译期间的递归溢出(递归次数超过编译器规定的最大递归次数)。如 GCC 会报告:fatal error: template instantiation depth exceeds maximum of 900 (use -ftemplate-depth= to increase the maximum)
如果此时将value类型改为unsigned,依旧输入负数编译,
不同的编译器和不同的标准选项会导致不同的结果。有些情况下错误信息完全不变,有些情况下则会报负数不能转换到 unsigned
最好的规避输入负数导致递归溢出的方法是,使用 static_assert,确保参数永远不会是负数。
template <int n>
struct factorial {
static_assert(
n >= 0,
"Arg must be non-negative");//static_assert对输入进行断言确保输入不是负数
static const int value =
n * factorial<n - 1>::value;
};
下面我们看一些更复杂的例子。这些例子不是为了让你真的去写这样的代码,而是帮助你充分理解编译期编程的强大威力。如果这些例子你都完全掌握了,那以后碰到小的模板问题,你一定可以轻松解决,完全不在话下。
我们可以看到,要进行编译期编程,最主要的一点,是需要把计算转变成类型推导。比如,下面的模板可以代表条件语句:
从编程范式上来说,C++模板元编程是函数式编程,用递归形式实现循环结构的功能,用C++ 模板的特化提供了条件判断能力,这两点使得其具有和普通语言一样通用的能力(图灵完备性)。
模版元程序由元数据和元函数组成,元数据就是元编程可以操作的数据,即C++编译器在编译期可以操作的数据。
元数据不是运行期变量,只能是编译期常量,不能修改,常见的元数据有enum枚举常量、静态常量、基本类型和自定义类型等。
浮点数由于精度问题,目前在 C++ 里不允许用作模板参数
元函数是模板元编程中用于操作处理元数据的“构件”,可以在编译期被“调用”,因为它的功能和形式 和 运行时的函数类似,而被称为元函数,它是元编程中最重要的构件。
元函数实际上表现为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可以通过type_traits—std11来实现,它不仅仅可以在编译期做判断,还可以做计算、查询、转换和选择。
模板元中的for等循环逻辑可以通过递归、重载、和模板特化(偏特化)等方法实现。
在模版元程序的具体实现时,由于其执行完全是在编译期,所以不能使用运行期的一些语法,比如if-else、for和while等语句都不能用。这些控制逻辑需要通过特殊的方法来实现。
下面来具体讲下这些逻辑的模板实现
If 模板
If 模板有三个参数,第一个是布尔值,后面两个则是代表不同分支计算的类型,typename 代表所有未知类型,这个类型可以是我们上面定义的任何一个模板实例,包括 If 和 factorial
第一个struct只是声明了模板形式,我们不提供通用定义
template <bool cond,
typename Then,
typename Else>
struct If;//这里的If的I是大写的i,小写的if是关键词不能用
第一个偏特化是布尔模板参数为true对应的情形 定义结果的tpye为模板参数Then分支类型。
template <typename Then,
typename Else>
struct If<true, Then, Else> {
typedef Then type;
};
第二个偏特化是布尔模板参数为false对应的情形 定义结果的tpye为模板参数Else分支类型。
template <typename Then,
typename Else>
struct If<false, Then, Else> {
typedef Else type;
};
使用的If模板的例子
下面的函数和模板是基本等价的:
int foo(int n)
{
if (n == 2 || n == 3 || n == 5) {
return 1;
} else {
return 2;
}
}
template <int n>
struct Foo {
//用 :: 取一个成员类型、并且 :: 左边有模板参数的话,
//得额外加上 typename 关键字来标明结果是一个类型
typedef typename If<
(n == 2 || n == 3 || n == 5),
integral_constant<int, 1>,
integral_constant<int, 2>>::type //这个type就是 typedef Else type; typedef Then type;对else或then 类型 的重命名 因为 type 本质是Else 或Then ,而Else 或Then又是模板参数用typename Then,typename Else来声明的,所以type 本质是个模板参数,代表一种类型 。所以在声明时需要加上typename 来说明它是个类型
type;// 再一次对结果重命名
};
可任意通过Foo<3>::type::value((因为type 本质是Else 或Then,else或then是integral_constante这种类型的,value是integral_constante结构体的一个静态成员变量所以可以直接用结构体名integral_constante<…>==type ,tpye::value来得到这个静态成员value的值,value用integral_constant第二个模板参数初始化)来输出编译期计算的结果
template<typename _Tp, _Tp __v>
struct integral_constant
{
static constexpr _Tp value = __v;
.....
};
类模板版
#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; }; //类模版的偏特化; 如果第一个模版非类型参数为true,IF_<true, Then, Else>::reType的值为模版的第二个类型参数Then
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';
}
输出结果4
/分析最里面的一层:
IF_
如果sizeof(long long) == 4, 上面的表达式返回long long, 否则返回void
/本
实际上,从C++11开始,可以通过type_traits来实现。因为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。
再举一个使用的例子
实现if条件为真调用两数的加法 条件为假调用两数的减法
template<bool cond,
typename Then,
typename Else>
struct If;
template<typename Then,
typename Else>
struct If<true, Then, Else> {
typedef Then result;//命名为result不叫type
};
template<typename Then,
typename Else>
struct If<false, Then, Else> {
typedef Else result;
};
template<int nums1, int nums2>
struct Add_ {
static const int value = nums1 + nums2;//可以通过Add_<1,2>::value 来得到1+2的结果
};
template<int nums1, int nums2>
struct Sub_ {
static const int value = nums1 - nums2;//可以通过Sub_<1,2>::value 来得到1-2的结果
};
// 组合成我们要实现的功能addSub
template<bool cond, int nums1, int nums2>
struct addSub {
static const auto RES = If<cond, Add_<nums1, nums2>,
Sub_<nums1, nums2>>::result::value;//如果cond为真调用Add为假调用剑法
//result为else或than类型 即Add_<nums1, nums2>或Sub_<nums1, nums2>类型,因为value为这两个类型的静态成员 所以可以直接通过类型名::value来得到value的值
};
// 调用
cout << addSub<true, 10, 2>::RES << endl;//通过RES得到 条件判断后 执行Add_或Sub_结果值value
只需要封装If,就能调用If的功能
// 判断N为奇数还是偶数
template<int N>
struct isEven {
//typedef integral_constant<bool, false> false_type;
static const auto RES = If<N & 1 == 0, true_type, false_type>::result::value;
//result为If模板的结果 value为 true_type和 false_type的静态成员变量
};
//调用
cout << isEven<10>::RES << endl;
循环模板
template <bool condition,
typename Body>
struct WhileLoop;
cond_value为循环的条件(真或假),res_type代表退出循环时的状态——是一种类型,next_type——是一种类型代表下面循环执行一次时的状态
偏特化是condition模板参数为true对应的情形, 对于while来说condition为true是执行一次循环体所以Body为next_type
template <typename Body>
struct WhileLoop<true, Body> {
//当condition模板参数为false,执行的就是继续执行循环 对应的函数体
typedef typename WhileLoop<
Body::cond_value,//用Body::next_type::cond_value也正确 更好
typename Body::next_type>::type
type;
};
偏特化是condition模板参数为false对应的情形,直接把Body的res_type进行返回,便是最后的结果。
template <typename Body>
struct WhileLoop<false, Body> {
//用 :: 取一个成员类型、并且 :: 左边有模板参数的话,得额外加上 typename 关键字来标明结果是一个类型
//当condition模板参数为false,执行的就是退出循环 对应的函数体
typedef
typename Body::res_type type;
};
//调用循环的结构体 Body中要含cond_value
template <typename Body>
struct While {
//用 :: 取一个成员类型、并且 :: 左边有模板参数的话,得额外加上 typename 关键字来标明结果是一个类型
typedef typename WhileLoop<
Body::cond_value, Body>::type
type;
//在下面的例子中 Body 就是 SumLoop<0, n> 这个递归模板调用
//要求我们的Body中必须定义cond_value,我们也确实定义了cond_value就是n != 0;
//根据SumLoop<0, n>::cond_value为true或false 调用WhileLoop的两个偏特化版本
//如果cond_value为flase (n == 0) 此时应该要终止对SumLoop的递归展开
//调用 WhileLoop<false, SumLoop<result为某个值, 0--递归终止态条件>>
//在这个模板调用中 将SumLoop<result,0>::res_type(其实就是用result值初始化的一个常数模板)==也命名为WhileLoop<false, Body>::type == While <Body>::type-结果就保存在这里
//如果cond_value为true(n != 0) 调用 WhileLoop<true, SumLoop<result为某个值, n>>这个特化版本这个特化版本中调用了SumLoop<result为某个值, 某个值>::next_type == SumLoop<result + n, n - 1> 也就是进行了一层的模板递归展开
};
//先写一个简单的例子 求和循环求sum(1,n)
//SumLoop 中定义了我们需要的 循环条件 循环结果 循环时的状态 循环执行一次时的状态
template<int result, int n>
struct SumLoop {
// 循环的条件
static const bool cond_value =
n != 0;//条件模板
// 循环后的结果
static const int res_value =
result;//保存了上次递归运行的结果或就是初值0
// 循环时的状态
typedef integral_constant<
int, res_value>
res_type;//常数模板 用res_type::value来取得其中的值(其实就是res_value)
// 循环执行一次时的状态 这是递归调用
typedef SumLoop<result + n, n - 1>//模板递归展开来实现循环
next_type;
};
//用我们直接编程的方式来说明
template<int n>
struct Sum {
//相当于函数调用 SumLoop(0,n) 返回值存在type中
typedef SumLoop<0, n> type;//调用求和循环初始result为0,第二个参数为n
//
};
// 调用
cout << While<Sum<6>::type>::type::value << endl;
//写个简单的例子 将他的实例化模板展开写清楚
While< Sum<2>::type >::type::value 实例化(instantiation)过程
—> While< SumLoop<0, 2> >::type::value
—> WhileLoop<SumLoop<0, 2>::cond_value, SumLoop<0, 2>>::type::value
—> WhileLoop
—> WhileLoop
—> WhileLoop
—> WhileLoop
—> WhileLoop
—> WhileLoop
—> WhileLoop
—> SumLoop<3, -1>::res_type::value
—>integral_constant
—>3
在C++14之后,有了下面语法,因此上述调用可以被简化:
template<int n>
using Sum_t = SumLoop<0, n>;
//上面两句话 代替了上面的求和结构体定义
// 调用
cout << While<Sum_t<6>>::type::value << endl;
循环展开的本质是模板递归展开,直到一个特化版本的递归终止结束
举一个例子
#include <iostream>
//求1+...+N的值
template<int N> class sum
{
public: static const int ret = sum<N-1>::ret + N;//递归求和
};
//模板的特化版本 是递归(循环的终止条件)
template<> class sum<0>
{
//这个特版本中没有 继续递归调用模板sum,所以递归展开在这里终止
public: static const int ret = 0;
};
int main()
{
std::cout << sum<5>::ret <<std::endl;
//等同于
//int result = 0;
//while (n != 0) {
// result = result + n;
// n = n - 1;
//}
return 0;
}
程序输出: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 (效率低 模板元编程没有变量,只有常量,原本一个变量就ok的事情,对元编程来说变量变了几次 就需要几个常量,就需要这么多次的模板调用)。
函数式编程看上去似乎效率低下(因为它和数学接近,而不是和硬件工作方式接近),但有自己的优势:描述问题更加简洁清晰,没有可变的变量就没有数据依赖,方便进行并行化。
用 :: 取一个成员类型、并且 :: 左边有模板参数的话,得额外加上 typename 关键字来标明结果是一个类型
switch/case模板
同样可以通过模板特化来模拟实现编译期的switch/case分支功能。参考如下代码:
#include <iostream>
using namespace std;
template<int v> class Case //泛型模板--就是其他没有被特化的分支都会进这个模板 为default分支
{
public:
static inline void Run()
{
cout << "default case" << endl;
}
};
template<> class Case<1>//特化case1
{
public:
static inline void Run()
{
cout << "case 1" << endl;
}
};
template<> class Case<2>//特化case2
{
public:
static inline void Run()
{
cout << "case 2" << endl;
}
};
int main()
{
Case<2>::Run();
}
程序输出结果:
case 2