Bootstrap

编译期计算

        关于编译期计算,直接能够想到的应用是决定是否启用某个模板,或者多个模板之间做选择。但如果有足够多的信息,编译器甚至可以计算控制流的结果。

模板元编程

        模板元编程的简单例子,如下:

#include <iostream>

template <unsigned p, unsigned d>
struct DoIsPrimer {
    static constexpr bool value = (p % d != 0) && DoIsPrimer<p, d - 1>::value;
};

template <unsigned p>
struct DoIsPrimer<p, 2> {
    static constexpr bool value = (p % 2 != 0);
};

template <unsigned p>
struct IsPrimer {
    static constexpr bool value = DoIsPrimer<p, p / 2>::value;
};

template <>
struct IsPrimer<0> { static constexpr bool value = false; };

template <>
struct IsPrimer<1> { static constexpr bool value = false; };

template <>
struct IsPrimer<2> { static constexpr bool value = true; };

template <>
struct IsPrimer<3> { static constexpr bool value = true; };

//仅为证明自己算发生在编译阶段
template <unsigned p, typename enable = typename std::enable_if<IsPrimer<p>::value>::type>
class Primer {
};

int main(int argc, char **argv)
{
    Primer<3> primer3;
    Primer<9> primer9;
    
    return 0;
}

        上面的例子Primer<3> primer3;通过编译,Primer<9> primer9;编译报错,这足以证明IsPrimer在编译阶段完成了计算,其展开步骤如下:

    IsPrime<9>::value
=>  DoIsPrime<9,4>::value
=>  9%4!=0 && DoIsPrime<9,3>::value
=>  9%4!=0 && 9%3!=0 && DoIsPrime<9,2>::value
=>  9%4!=0 && 9%3!=0 && 9%2!=0
=>  false  

通过 constexpr 进行计算

        c++11开始引入了constexpr特性,大大简化了编译器运算。但对于constexpr使用,c++11拥有诸多限制,如constexpr的定义只能包含一个return语句。这些限制从c++14开始,大部分被移除。但为了所有计算步骤都能够在编译其进行,目前所有的c++版本constexpr函数都不支持异常抛出和内存分配。下面是constexpr版的IsPrimer:

#include <iostream>

#if 1
//c++11实现
constexpr bool DoIsPrimer(unsigned p, unsigned d) { return d != 2 ? (p % d != 0) && DoIsPrimer(p, d - 1) : (p % 2 != 0); }

constexpr bool IsPrimer(unsigned p) { return p < 4 ? !(p < 2) : DoIsPrimer(p, p / 2); }
#else
//c++14实现
constexpr bool IsPrimer(unsigned p) {
    for (unsigned d = 2; d < p / 2; ++d) {
        if (p % d == 0)
            return false;
    }
    return p > 1;
}
#endif

//仅为证明自己算发生在编译阶段
template <unsigned p, typename enable = typename std::enable_if<IsPrimer(p)>::type>
class Primer {
};

int main(int argc, char **argv)
{
    Primer<3> primer3;
    Primer<9> primer9;
    return 0;
}

         需要注意一点,“可以”在编译期计算,并非“一定”在编译期计算。计算发生在编译还是运行,先来做一个实验:

//...
bool is_p0 = IsPrimer(0); //==在编译期计算
const bool is_p1 = IsPrimer(1); //==在编译期计算
constexpr bool is_p2 = IsPrimer(2); //==在编译期计算
int p3 = 3;
bool is_p3 = IsPrimer(p3); //在运行期计算
int p4 = 4;
const bool is_p4 = IsPrimer(p4); //在运行期计算
const int p5 = 5;
constexpr bool is_p5 = IsPrimer(p5); //==在编译期计算

int main(int argc, char **argv)
{
    Primer<3> primer3;
    bool is_p6 = IsPrimer(6); //在运行期计算
    int p7 = 7;
    bool is_primer = IsPrimer(p7); //在运行期计算
    const int p8 = 8;
    constexpr bool is_p8 = IsPrimer(p8); //==在编译期计算
    return 0;
}

        关于上面的实验, 通过在IsPrime设置断点即可判断计算发生在编译期还是运行期。通过实验,可以发现下面的规律:

  • 如果计算在全局,结果变量为constexpr或输入参数为常量,计算都会在编译期触发
  • 如果计算在局部,结果变量为constexpr并且输入参数为常量,计算才会在编译期触发,否则延迟到运行期

编译器if

        c++17引入了if constexpr(...)语法,编译器根据该语法在编译期决定使用if部分还是else部分的代码。该语法主要用于两个场景:

  • 变参模板

        使用编译器if判断参数数量,当参数为0时,不再执行任何语句,解除对void print(void)函数的依赖。此处编译期if不可被普通if取代,print函数族的迭代生成实在编译器完成的,如果使用普通if,编译时会报错“No matching function for call to 'print' ”,因为print模板不会生成参数为0的函数,当编译迭代到...args为0时,发现没有合适的函数调用报错。

#include <iostream>

template <typename T, typename ...Ts>
void print(const T &arg, const Ts &...args)
{
    std::cout << arg << std::endl;
    if constexpr(sizeof...(args) > 0) {
        print(args...);
    }
}


int main(int argc, char **argv)
{
    print("Jim", 'M', 30);
    return 0;
}
  • std::is_xxx函数族

        通过使用std::is_xxx函数族判断,不同的的情况使用不同的代码。下面是一个例子,如果T是整形族,则执行递归;否则直接返回原值。此处是否可用普通if语句代替编译器if语句?答案是否定的。如果使用普通if语句,编译时,整个函数的所有语句都会被编译,当类型为string时,由于不支持和整型的比较,及-和*操作符,arg == 1和arg * factorial(arg - 1)都会报错。

template <typename T>
constexpr T factorial(T arg)
{
    if constexpr(std::is_integral_v<T>) {
        if (arg == 1)
            return arg;
        return arg * factorial(arg - 1);
    }
    else {
        ///static_assert(false, "argument is not integral!"); ///编译期总是会被触发,不管是否会用到该段代码
        static_assert(!std::is_integral_v<T>, "argument is not integral!"); //总是不会被触发,即便该段代码被用到
        std::cout << arg<< std::endl;
        return arg;
    }
}

int main(int argc, char **argv)
{
    std::cout << factorial(5) << std::endl;
    std::cout << factorial("hello") << std::endl;
    return 0;
}

;