C++编译期计算¶
今天学到了一个让我看代码时很烧脑,但是在一步步模拟程序的过程中让我恍然大悟的知识点。是关于模板元编程的使用。模板元编程在我看来是利用了模板的特性,对模板进行特化,在执行不同类型的数据计算时比较通用的方式
这是一个简单的体现模板元的例子:
template<int n>
struct factorial{
static const int value= n * factorial< n - 1 >::value; // 模板的参数一般设置为static,在实例化时不属于该类
};
tempalte<>
struct factorial{
static const int value = 1;
};
上面的模板实现了从n*(n-1)
递减乘积的算法。先写模板的泛化,再写模板的特化。特化是在n递减到 1时,value的值
如果传入的模板参数是负数,结果会如何?
结果肯定是堆栈溢出。n将无限的递减下去。
最好的能避免类型带来的其他编译错误的解决方案是,使用satic_assert
,确保参数不会是负数
template<int n>
struct factorial{
static_assert( n>=0,"Arg must be non_negtive");
static const int value=n * factorial< n - 1>::value;
};
若有传入负数,则会报出static_assert
的错误
要进行编译期编程,最重要的一点是,将计算过程转换成类型推导!
// if 模板的泛化
template<bool cond,typename Then,typename Else>
struct If;
// 模板的特化:真
template<typename Then,typename Else>
struct If<true,Then,Else>{
typedef Then type;
};
// 模板的特化:假
template<typename Then,typename Else>
struct If<false,Then,Else>{
typedef Else type;
};
上述是if-else
的类型推导,接下来可能还需要while
循环:
// while 循环的泛化
// 第一个参数:表示条件真/假
// 第二个参数:循环体的内容
template<bool condition,typename Body>
struct WhileLoop;
// 特化版本
// 条件为真
template<typename Body>
struct WhileLoop<true,Body>{
typedef typename WhileLoop< Body::cond_value, typename Body::next_type >::type type;
};
// 特化版本
// 条件为假
template<typename Body>
struct WhileLoop<false,Body>{
typedef typename Body::res_type type;
};
// 相当于对WhileLoop的包装
template<typename Body>
struct While{
typedef typename WhileLoop< Body::cond_value,Body >::type type;
};
首先需要明确的是:
cond_value
:表示的是循环体条件的真假next_type
:表示的是执行下一次循环体之后的状态res_value
:退出循环体时的状态
这个循环的模板的类型转换我当时看着是有点晕的,但是将核心的转换看清楚还是很有趣的:
- 在特化条件为真的版本中,定义了一个特化模板类型
WhileLoop
;循环体的循环判断条件是根据内容的条件来决定的Body::cond_value
;由于是条件为真,所以需要不断判断执行下一次循环体的条件状态,所以需要第二个参数Body::next_value
- 在特化条件为假的版本中,由于条件为假,所以需要知道退出时的状态
Body::res_type
,这就不再需要调用自身模板了,毕竟执行完就结束了 - 接下来是一个类似于对
WhileLoop
的包装
现在写好了条件判断等语句的模板,为了进行计算,我们需要写一个通用的代表类型 :以整型常数类型为例
template<class T,T v>
struct integral_constant{
static const T value= v;
typedef T value_type;
typedef integral_constant type;
}; // 包含了值类型和数值
写一个模板来实现从 1加到 n:
template<int result,int n>
struct SumLoop{
static const bool cond_value = n! = 0; // 循环体判断条件:cond_value
static const int res_value = result; // 每一次计算的结果值
typedef integral_constant< int, res_value > res_type; // 放入通用类型:获得值跟类型,循环体退出的状态
typedef SumLoop< result + n, n - 1 > next_type; // 从大到小递减累加,循环体执行下一次的状态
};
// 对SumLoop的封装
template<int n>
struct Sum{
typedef SumLoop<0,n> type;
};
我们就可以通过:
计算出从 1加到 10的结果
我们可以从调用来看,是如何实现的:
- 首先,我们是利用类型推导来进行计算的,就是编译期编程
- 所以我们直接使用
While<>
来实现这个计算。传入的参数:Sum<>
计算的模板算法;这个模板算法传入的参数:10(由上述代码得出) - 我们需要使用包含在
While
中的typedef typename WhileLoop< Body::cond_value,Body >::type type
这个type变量,来继续调用真实的循环执行 - 同样
Sum<10>::type
是为了调用包含于Sum中的 type这个变量,调用真正的SumLoop的实现 - 通过
template<bool condition,typename Body>
我们可以知道While<>
中的参数是一个判断真假,一个循环体的内容,所以SumLoop
中的:cond_value
、res_type
、next_type
都提供了。执行WhileLoop
- 接下来就是单层逻辑的分析了:
- 当 n>0 时,
cond_value
为真,循环体调用WhileLoop<true,Body>
这个循环体,直到在后续递减为 0 时,条件置为假;当 n>0 时,更新的next_type
状态总是能被WhileLoop<true,Body>
调用 cond_value
为假时,res_type
出现,调用WhileLoop<false,Body>
的版本,然后不再调用自身循环,退出时的状态res_type
中取得的res_value
就是计算结果
有需要注意的是:
- 像
Body::cond_value
中::
是该对象的某个成员函数,取出的是一个模板参数,严格上需要在前面加上typename
,即typename Body::cond_value
以上还是C++ 98的性质。确实有点麻烦 : D
不过理解这些我想对于理解模板元编程会更进一步
C++11 对应的新工具¶
标准库定义了一些新的工具:
typedef std::integral_constant< bool, true > true_type;
typedef std::integral_constant< bool, false > false_type;
来看一个例子:
template<class T>
class SomeContainer{
public:
...
static void destroy(T* ptr){
_destroy(ptr,
is_trivially_destructible< T >());
}
private:
static void _destory(T* ptr, true_type){}
static void _destroy(T* ptr,false_type){
ptr->~T();
}
};
上述程序:
is_trivially_destructible<T>
:此模板用来判断类是否有必要析构。因为有些数据类型不进行析构也是不会带来问题的,像内置数据类型。以此来节省析构的开销- 如果传入的类型没必要析构,则为
true_type
;如果传入类型确实需要析构,比如自定义数据类型,则为false_type
还有一些模板可以作为类型转换:
template<typename T>
struct remove_const{
typedef T type;
};
template<typename T>
struct remove_const<const T>{
typedef T type;
};
-
可以通过
remove_const
将const类型转换为非const的 -
const string&
经过转换成为string&
,即remove_const<const string&> :: type
等价于string&
-
值的注意的是:
const char*
经过转换后还是const char*
因为remove_const
的转换针对的是所谓的“指针常量”,如果转换的是char* const
还是可以使之成为char*
它针对的是:类型本身是常量的指针,而非指向常量的指针类型
还是有简化版本的,这来源于标准库。毕竟用remove_const<T>::type
和is_trivially_destructible<T>::type
还是有点麻烦的hh
template<class T>
inline constexpr bool is_trivially_destructible_v
= is_trivially_destructible<T>::type;
利用is_trivially_destructible_v
来表示编译时常量
利用remove_const_t
来表示编译时类型别名
using
和typedef
的区别:
typedef
只能针对某个特定的类型定义
using
可以生成别名模板
通用的fmap模板¶
演示一个map函数
template<template<typename,typename>class OutContainer = vector,typename F, class R>
auto fmap(F&& f,R&& inputs){
typedef decay_t<decltype( f(*inputs.begin()))> resulit_type;
OutContainer<result_type,alloactor<result_type>> result;
for(auto&& item: inputs){
result.push_bakc(f(item));
}
return result;
}
这里我其实也看着有点费劲hh
- 首先是模板定义,有几个参数:
- 一个容器,这里是
vector
,容器的参数:值类型,分配器类型 - F在这是个函数类型
- R是个元素类型
- fmap函数传入参数类型F的函数 f ,和类型R的元素inputs
- 定义
result_type
表示:decay_t()
将decltype()
获取的inputs
第一个元素的迭代器的元素类型,转换成普通的值类型 - 在创建一个
vector
容器OutContainer
(值类型和分配器类型为上述的result_type
) - for循环将
f()
调用的inpus
的元素放入容器中 - 这里都使用了右值引用,可能是为了实现完美转发