跳转至

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;
};

我们就可以通过:

While<Sum<10>::type>::type::value

计算出从 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_valueres_typenext_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>::typeis_trivially_destructible<T>::type还是有点麻烦的hh

template<class T>
    inline constexpr bool is_trivially_destructible_v 
        = is_trivially_destructible<T>::type;

利用is_trivially_destructible_v来表示编译时常量

template<class T>
    using remove_const_t
        = remove_const<T>::type;

利用remove_const_t来表示编译时类型别名

usingtypedef的区别:

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的元素放入容器中
  • 这里都使用了右值引用,可能是为了实现完美转发