编译期计算
    模板的另外一种重要用途——编译期计算,也称作“模板元编程”。
    理论上使用c++模板,可以在编译期完成任何的计算任务,现实中绝对不会有人会这么做,针对编译期的编程基本没人看的懂,但即使这样我们依旧还是要了解一下模板元编程的基本概念:它仍然有一些实用的场景,并且在实际的工程中你也可能会遇到这样的代码。
    编译期计算基本就是借助模板参数递归计算来得到结果
    //先定义模板函数

    1. template <int n>
    2. struct factorial {
    3. //可以用静态常量或者enum hack定义编译期计算变量value
    4. static const int value =
    5. n * factorial<n - 1>::value;
    6. };
    7. //在特化 n = 0时的情况
    8. template <>
    9. struct factorial<0> {
    10. static const int value = 1;
    11. };


    上面定义了一个递归形式的 阶乘函数 (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,确保参数永远不会是负数。

    1. template <int n>
    2. struct factorial {
    3. static_assert(
    4. n >= 0,
    5. "Arg must be non-negative");//static_assert对输入进行断言确保输入不是负数
    6. static const int value =
    7. n * factorial<n - 1>::value;
    8. };


    下面我们看一些更复杂的例子。这些例子不是为了让你真的去写这样的代码,而是帮助你充分理解编译期编程的强大威力。如果这些例子你都完全掌握了,那以后碰到小的模板问题,你一定可以轻松解决,完全不在话下。

    我们可以看到,要进行编译期编程,最主要的一点,是需要把计算转变成类型推导。比如,下面的模板可以代表条件语句:

    从编程范式上来说,C++模板元编程是函数式编程,用递归形式实现循环结构的功能,用C++ 模板的特化提供了条件判断能力,这两点使得其具有和普通语言一样通用的能力(图灵完备性)。
    模版元程序由元数据和元函数组成,元数据就是元编程可以操作的数据,即C++编译器在编译期可以操作的数据。
    元数据不是运行期变量,只能是编译期常量,不能修改,常见的元数据有enum枚举常量、静态常量、基本类型和自定义类型等。
    浮点数由于精度问题,目前在 C++ 里不允许用作模板参数
    元函数是模板元编程中用于操作处理元数据的“构件”,可以在编译期被“调用”,因为它的功能和形式 和 运行时的函数类似,而被称为元函数,它是元编程中最重要的构件。
    元函数实际上表现为C++的一个类、模板类或模板函数,它的通常形式如下:
    //一个很简单明了的 元模板函数

    1. template<int N, int M>//类型被当成变量来用
    2. struct meta_func
    3. {
    4. static const int value = N+M;
    5. }
    6. //调用元函数获取value值:
    7. cout<<meta_func<1, 2>::value<<endl;


    meta_func的执行过程是在编译期完成的,实际执行程序时,是没有计算动作而是直接使用编译期的计算结果。元函数只处理元数据,元数据是编译期常量和类型,所以下面的代码是编译不过的:

    1. int i = 1, j = 2;
    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只是声明了模板形式,我们不提供通用定义

    1. template <bool cond,
    2. typename Then,
    3. typename Else>
    4. struct If;//这里的If的I是大写的i,小写的if是关键词不能用



    第一个偏特化是布尔模板参数为true对应的情形 定义结果的tpye为模板参数Then分支类型。

    1. template <typename Then,
    2. typename Else>
    3. struct If<true, Then, Else> {
    4. typedef Then type;
    5. };


    第二个偏特化是布尔模板参数为false对应的情形 定义结果的tpye为模板参数Else分支类型。

    1. template <typename Then,
    2. typename Else>
    3. struct If<false, Then, Else> {
    4. typedef Else type;
    5. };

    使用的If模板的例子

    下面的函数和模板是基本等价的:

    1. int foo(int n)
    2. {
    3. if (n == 2 || n == 3 || n == 5) {
    4. return 1;
    5. } else {
    6. return 2;
    7. }
    8. }
    9. template <int n>
    10. struct Foo {
    11. //用 :: 取一个成员类型、并且 :: 左边有模板参数的话,
    12. //得额外加上 typename 关键字来标明结果是一个类型
    13. typedef typename If<
    14. (n == 2 || n == 3 || n == 5),
    15. integral_constant<int, 1>,
    16. 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 来说明它是个类型
    17. type;// 再一次对结果重命名
    18. };


    可任意通过Foo<3>::type::value((因为type 本质是Else 或Then,else或then是integral_constante这种类型的,value是integral_constante结构体的一个静态成员变量所以可以直接用结构体名integral_constante<…>==type ,tpye::value来得到这个静态成员value的值,value用integral_constant第二个模板参数初始化)来输出编译期计算的结果

    1. template<typename _Tp, _Tp __v>
    2. struct integral_constant
    3. {
    4. static constexpr _Tp value = __v;
    5. .....
    6. };



    类模板版

    1. #include <iostream>
    2. template<bool c, typename Then, typename Else> class IF_ {}; //基础类模版
    3. template<typename Then, typename Else>
    4. class IF_<true, Then, Else> { public: typedef Then reType; }; //类模版的偏特化; 如果第一个模版非类型参数为true,IF_<true, Then, Else>::reType的值为模版的第二个类型参数Then
    5. template<typename Then, typename Else>
    6. class IF_<false,Then, Else> { public: typedef Else reType; }; //类模版的偏特化
    7. int main()
    8. {
    9. const int len = 4;
    10. // 定义一个指定字节数的类型
    11. typedef
    12. IF_<sizeof(short)==len, short,
    13. IF_<sizeof(int)==len, int,
    14. IF_<sizeof(long)==len, long,
    15. IF_<sizeof(long long)==len, long long,
    16. void>::reType>::reType>::reType>::reType int_my;
    17. std::cout << sizeof(int_my) << '\n';
    18. }

    输出结果4
    /分析最里面的一层:
    IF_::reType
    如果sizeof(long long) == 4, 上面的表达式返回long long, 否则返回void
    /本

    实际上,从C++11开始,可以通过type_traits来实现。因为type_traits提供了编译期选择特性:std::conditional,它在编译期根据一个判断式选择两个类型中的一个,和条件表达式的语义类似,类似于一个三元表达式。它的原型是:

    1. template< bool B, class T, class F >
    2. struct conditional;
    3. 所以上面的代码可以改写为如下代码:
    4. #include <iostream>
    5. #include <type_traits>
    6. int main()
    7. {
    8. const int len = 4;
    9. // 定义一个指定字节数的类型
    10. typedef
    11. std::conditional<sizeof(short)==len, short,
    12. std::conditional<sizeof(int)==len, int,
    13. std::conditional<sizeof(long)==len, long,
    14. std::conditional<sizeof(long long)==len, long long,
    15. void>::type>::type>::type>::type int_my;
    16. std::cout << sizeof(int_my) << '\n';
    17. }

    程序同样编译输出4。


    再举一个使用的例子
    实现if条件为真调用两数的加法 条件为假调用两数的减法

    1. template<bool cond,
    2. typename Then,
    3. typename Else>
    4. struct If;
    5. template<typename Then,
    6. typename Else>
    7. struct If<true, Then, Else> {
    8. typedef Then result;//命名为result不叫type
    9. };
    10. template<typename Then,
    11. typename Else>
    12. struct If<false, Then, Else> {
    13. typedef Else result;
    14. };
    15. template<int nums1, int nums2>
    16. struct Add_ {
    17. static const int value = nums1 + nums2;//可以通过Add_<1,2>::value 来得到1+2的结果
    18. };
    19. template<int nums1, int nums2>
    20. struct Sub_ {
    21. static const int value = nums1 - nums2;//可以通过Sub_<1,2>::value 来得到1-2的结果
    22. };
    23. // 组合成我们要实现的功能addSub
    24. template<bool cond, int nums1, int nums2>
    25. struct addSub {
    26. static const auto RES = If<cond, Add_<nums1, nums2>,
    27. Sub_<nums1, nums2>>::result::value;//如果cond为真调用Add为假调用剑法
    28. //result为else或than类型 即Add_<nums1, nums2>或Sub_<nums1, nums2>类型,因为value为这两个类型的静态成员 所以可以直接通过类型名::value来得到value的值
    29. };
    30. // 调用
    31. cout << addSub<true, 10, 2>::RES << endl;//通过RES得到 条件判断后 执行Add_或Sub_结果值value

    只需要封装If,就能调用If的功能
    // 判断N为奇数还是偶数

    1. template<int N>
    2. struct isEven {
    3. //typedef integral_constant<bool, false> false_type;
    4. static const auto RES = If<N & 1 == 0, true_type, false_type>::result::value;
    5. //result为If模板的结果 value为 true_type和 false_type的静态成员变量
    6. };
    7. //调用
    8. cout << isEven<10>::RES << endl;


    循环模板

    1. template <bool condition,
    2. typename Body>
    3. struct WhileLoop;

    cond_value为循环的条件(真或假),res_type代表退出循环时的状态——是一种类型,next_type——是一种类型代表下面循环执行一次时的状态

    偏特化是condition模板参数为true对应的情形, 对于while来说condition为true是执行一次循环体所以Body为next_type

    1. template <typename Body>
    2. struct WhileLoop<true, Body> {
    3. //当condition模板参数为false,执行的就是继续执行循环 对应的函数体
    4. typedef typename WhileLoop<
    5. Body::cond_value,//用Body::next_type::cond_value也正确 更好
    6. typename Body::next_type>::type
    7. type;
    8. };


    偏特化是condition模板参数为false对应的情形,直接把Body的res_type进行返回,便是最后的结果。

    1. template <typename Body>
    2. struct WhileLoop<false, Body> {
    3. //用 :: 取一个成员类型、并且 :: 左边有模板参数的话,得额外加上 typename 关键字来标明结果是一个类型
    4. //当condition模板参数为false,执行的就是退出循环 对应的函数体
    5. typedef
    6. typename Body::res_type type;
    7. };


    //调用循环的结构体 Body中要含cond_value

    1. template <typename Body>
    2. struct While {
    3. //用 :: 取一个成员类型、并且 :: 左边有模板参数的话,得额外加上 typename 关键字来标明结果是一个类型
    4. typedef typename WhileLoop<
    5. Body::cond_value, Body>::type
    6. type;
    7. //在下面的例子中 Body 就是 SumLoop<0, n> 这个递归模板调用
    8. //要求我们的Body中必须定义cond_value,我们也确实定义了cond_value就是n != 0;
    9. //根据SumLoop<0, n>::cond_value为true或false 调用WhileLoop的两个偏特化版本
    10. //如果cond_value为flase (n == 0) 此时应该要终止对SumLoop的递归展开
    11. //调用 WhileLoop<false, SumLoop<result为某个值, 0--递归终止态条件>>
    12. //在这个模板调用中 将SumLoop<result,0>::res_type(其实就是用result值初始化的一个常数模板)==也命名为WhileLoop<false, Body>::type == While <Body>::type-结果就保存在这里
    13. //如果cond_value为true(n != 0) 调用 WhileLoop<true, SumLoop<result为某个值, n>>这个特化版本这个特化版本中调用了SumLoop<result为某个值, 某个值>::next_type == SumLoop<result + n, n - 1> 也就是进行了一层的模板递归展开
    14. };


    //先写一个简单的例子 求和循环求sum(1,n)
    //SumLoop 中定义了我们需要的 循环条件 循环结果 循环时的状态 循环执行一次时的状态

    1. template<int result, int n>
    2. struct SumLoop {
    3. // 循环的条件
    4. static const bool cond_value =
    5. n != 0;//条件模板
    6. // 循环后的结果
    7. static const int res_value =
    8. result;//保存了上次递归运行的结果或就是初值0
    9. // 循环时的状态
    10. typedef integral_constant<
    11. int, res_value>
    12. res_type;//常数模板 用res_type::value来取得其中的值(其实就是res_value)
    13. // 循环执行一次时的状态 这是递归调用
    14. typedef SumLoop<result + n, n - 1>//模板递归展开来实现循环
    15. next_type;
    16. };
    17. //用我们直接编程的方式来说明
    18. template<int n>
    19. struct Sum {
    20. //相当于函数调用 SumLoop(0,n) 返回值存在type中
    21. typedef SumLoop<0, n> type;//调用求和循环初始result为0,第二个参数为n
    22. //
    23. };
    24. // 调用
    25. 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>::type::value

    —> WhileLoop::cond_value, SumLoop<0, 2>::next_type>::type::value
    —> WhileLoop>::type::value

    —> WhileLoop::cond_value, SumLoop<2, 1>::next_type>::type::value
    —> WhileLoop>::type::value

    —> WhileLoop::cond_value, SumLoop<3, 0>::next_type>::type::value
    —> WhileLoop>::type::value

    —> SumLoop<3, -1>::res_type::value

    —>integral_constant::value
    —>3


    在C++14之后,有了下面语法,因此上述调用可以被简化:

    1. template<int n>
    2. using Sum_t = SumLoop<0, n>;
    3. //上面两句话 代替了上面的求和结构体定义
    4. // 调用
    5. cout << While<Sum_t<6>>::type::value << endl;


    循环展开的本质是模板递归展开,直到一个特化版本的递归终止结束
    举一个例子

    1. #include <iostream>
    2. //求1+...+N的值
    3. template<int N> class sum
    4. {
    5. public: static const int ret = sum<N-1>::ret + N;//递归求和
    6. };
    7. //模板的特化版本 是递归(循环的终止条件)
    8. template<> class sum<0>
    9. {
    10. //这个特版本中没有 继续递归调用模板sum,所以递归展开在这里终止
    11. public: static const int ret = 0;
    12. };
    13. int main()
    14. {
    15. std::cout << sum<5>::ret <<std::endl;
    16. //等同于
    17. //int result = 0;
    18. //while (n != 0) {
    19. // result = result + n;
    20. // n = n - 1;
    21. //}
    22. return 0;
    23. }

    程序输出: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分支功能。参考如下代码:

    1. #include <iostream>
    2. using namespace std;
    3. template<int v> class Case //泛型模板--就是其他没有被特化的分支都会进这个模板 为default分支
    4. {
    5. public:
    6. static inline void Run()
    7. {
    8. cout << "default case" << endl;
    9. }
    10. };
    11. template<> class Case<1>//特化case1
    12. {
    13. public:
    14. static inline void Run()
    15. {
    16. cout << "case 1" << endl;
    17. }
    18. };
    19. template<> class Case<2>//特化case2
    20. {
    21. public:
    22. static inline void Run()
    23. {
    24. cout << "case 2" << endl;
    25. }
    26. };
    27. int main()
    28. {
    29. Case<2>::Run();
    30. }

    程序输出结果:
    case 2