编译期计算

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

模板元编程

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

cpp 复制代码
#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在编译阶段完成了计算,其展开步骤如下:

bash 复制代码
    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:

cpp 复制代码
#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;
}

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

cpp 复制代码
//...
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时,发现没有合适的函数调用报错。

cpp 复制代码
#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)都会报错。

cpp 复制代码
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;
}
相关推荐
学习路上_write5 分钟前
FREERTOS_互斥量_创建和使用
c语言·开发语言·c++·stm32·单片机·嵌入式硬件
闻缺陷则喜何志丹1 小时前
【SOSDP模板 容斥原理 逆向思考】3757. 有效子序列的数量|分数未知
c++·算法·力扣·容斥原理·sosdp·逆向思考
BestOrNothing_20152 小时前
一篇搞懂 C++ 重载:函数重载 + 运算符重载,从入门到会用(含 ++、<<、== 实战)
c++·函数重载·运算符重载·operator·前置后置++·重载与重写
2501_941144422 小时前
Python + C++ 异构微服务设计与优化
c++·python·微服务
程序猿编码2 小时前
PRINCE算法的密码生成器:原理与设计思路(C/C++代码实现)
c语言·网络·c++·算法·安全·prince
charlie1145141913 小时前
深入理解C/C++的编译链接技术6——A2:动态库设计基础之ABI设计接口
c语言·开发语言·c++·学习·动态库·函数
Cx330❀3 小时前
C++ STL set 完全指南:从基础用法到实战技巧
开发语言·数据结构·c++·算法·leetcode·面试
zmzb01033 小时前
C++课后习题训练记录Day33
开发语言·c++
Want5953 小时前
C/C++贪吃蛇小游戏
c语言·开发语言·c++
草莓熊Lotso4 小时前
《算法闯关指南:动态规划算法--斐波拉契数列模型》--01.第N个泰波拉契数,02.三步问题
开发语言·c++·经验分享·笔记·其他·算法·动态规划