constexpr 全家桶

前言

C++98/03 时代,"常量"只能是字面量或简单表达式,很多数组大小、模板参数、switch case的计算必须在运行时做,导致:

  • 性能损失(运行时计算)

  • 无法用于模板、数组大小、static_assert 等编译期场景

  • 代码里到处是 magic number 或运行时 assert

在 C++11 之前,若想在编译期计算阶乘,只能用模板特化,晦涩难懂

cpp 复制代码
// C++98 模板元编程实现编译期阶乘
template <int N>
struct Factorial {
    enum { value = N * Factorial<N-1>::value };
};
template <> // 特化:递归终止条件
struct Factorial<0> {
    enum { value = 1 };
};

int arr[Factorial<5>::value]; // 数组大小 120,但写法像"黑话"

C++11 委员会决定引入 constexpr:让函数和变量在编译期就能计算。目标是"把能放到编译期的计算全部提前",实现零运行时开销 + 更强的类型安全。C++14 进一步放松规则,允许 if、for、局部变量,C++17 加入 if constexpr编译期分支,让模板元编程从"黑魔法"变成"正常写法"。

constexpr 的设计目标很明确:把能放到编译期的计算全部提前,实现零运行时开销 + 更强的类型安全。


constexpr 变量 + 函数(C++11 基础)

底层原理:

  • constexpr 告诉编译器:"这个东西必须在编译期算出来,否则编译错误。"

  • 编译器会像执行一个 mini 解释器一样在编译阶段跑你的函数,算出结果,存到二进制里(零运行时开销)。

  • 只能用编译期已知的操作(不能 new、不能 throw、不能调用非 constexpr 函数)。

C++11 中 constexpr 函数的约束非常苛刻:

  • 函数体只能包含一条 return 语句(不能有其他语句)

  • 不能定义局部变量

  • 不能有循环

  • 不能有 if,只能用三元运算符 (?:)

  • 只能调用其他 constexpr 函数

  • 只能操作字面量类型,算术类型、引用、指针等,自定义类型需满足特定条件

代码示例:

cpp 复制代码
// C++98 老写法(运行时计算)
const int size = 10 + 20;  // 仅为"只读",非编译期常量
int arr[size];              //错误,size 不是编译期常量

// C++11 新写法(编译期计算)
constexpr int size = 10 + 20;  // 编译期直接算出 30
int arr[size];                   // 完全合法

constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n-1);
}

int main() {
    int arr2[factorial(5)];                // 编译期算出 120,数组大小直接确定
    static_assert(factorial(5) == 120, ""); // 编译期断言,不通过则报错
}

C++11 constexpr 类与构造函数

C++11 不仅允许 constexpr 变量和函数,还引入了 constexpr 构造函数,只要其构造函数是 constexpr 且所有成员都是字面量类型,允许在编译期创建对象:

cpp 复制代码
class Point {
public:
    // constexpr 构造函数:必须在初始化列表中完成所有成员初始化
    constexpr Point(int x, int y) noexcept : x_(x), y_(y) {}

    // constexpr 成员函数:必须是 const 的(C++11 限制,C++14 可放松)
    constexpr int getX() const noexcept { return x_; }
    constexpr int getY() const noexcept { return y_; }

private:
    int x_, y_;
};

// 编译期创建对象
constexpr Point origin(0, 0);
constexpr Point p(10, 20);

// 直接用于数组大小、模板参数等编译期场景
int arr[p.getX()];                // 合法:数组大小为 10
static_assert(origin.getY() == 0); // 编译期断言通过
  • constexpr 构造函数的函数体在 C++11 中必须为空,所有初始化必须在初始化列表完成。

  • constexpr 对象的成员变量必须在编译期初始化。


C++14 constexpr 的放松

C++11 里 constexpr 函数只能有一条 return 语句,不能有 if、for、局部变量。C++14 对 constexpr 函数的限制做了大幅松绑,使其几乎可以像普通函数一样编写:

  • 可以写 if、for、while、switch

  • 可以有局部变量,但必须是字面量类型,不能是 std::string 等非字面量类型。

  • 可以调用其他 constexpr 函数

  • 可以有多条 return 语句

  • 允许 constexpr 成员函数

限制项 C++11 C++14
函数体语句 只能有一条 return 语句 允许 iffor、while、局部变量等
局部变量 不允许 允许,但必须初始化
修改成员变量的函数 必须是 const 允许非 const(可修改对象状态)

对上述代码进行改造:

cpp 复制代码
// C++11 老写法(只能一条 return)
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n-1);
}

// C++14 新写法(可以写循环、局部变量)
constexpr int sum_up_to(int n) {
    int sum = 0;
    for (int i = 1; i <= n; ++i) {
        sum += i;
    }
    return sum;
}

constexpr int s = sum_up_to(100);   // 编译期算出 5050

除此之外,再提供一个编译期计算最大公约数的例子:

cpp 复制代码
constexpr int gcd(int a, int b) noexcept {
    while (b != 0) {
        int temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}

static_assert(gcd(48, 18) == 6); // 编译期验证通过

注意,constexpr 函数里不能调用非 constexpr 函数,不能使用 new/delete、throw、goto 等。


C++17 if constexpr

设计原因: 以前模板里要根据类型做不同行为,只能用 SFINAE或 std::enable_if,代码又臭又长。

底层原理: if constexpr 的不满足分支在编译期被直接丢弃,不会生成代码,不会实例化模板,彻底解决 "死代码" 问题。

代码示例:

cpp 复制代码
#include <iostream>
#include <type_traits> // 类型特征头文件,后面会详细讲

template<typename T>
void print(T value) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << "整数: " << value << '\n';
    } else if constexpr (std::is_floating_point_v<T>) {
        std::cout << "浮点: " << value << '\n';
    } else {
        std::cout << "其他类型: " << value << '\n';
    }
}

int main() {
    print(42);          // 输出"整数: 42"
    print(3.14);        // 输出"浮点: 3.14"
    print("hello");     // 输出"其他类型: hello"
}

if constexpr 的优势,不满足分支不实例化

普通 if 的所有分支都会被编译,即使条件不满足,而 if constexpr 的不满足分支会被直接丢弃,因此即使分支里的代码对当前类型不合法也不会报错:

cpp 复制代码
template <typename T>
auto get_value(T t) {
    if constexpr (std::is_pointer_v<T>) {
        return *t; // 如果 T 不是指针,这行不会被实例化,所以不会报错!
    } else {
        return t;
    }
}

int main() {
    int x = 42;
    get_value(&x); // 返回 42(T 是 int*,走第一个分支)
    get_value(x);  // 返回 42(T 是 int,走第二个分支,不会因 *t 报错)
}

常见坑

  • constexpr 函数里调用了非 constexpr 函数 → 编译错误

  • constexpr 变量没初始化或初始化用了运行时值 → 错误

  • if constexpr 里放运行时表达式 → 错误,必须是编译期常量表达式

  • constexpr 函数递归深度限制→ 编译器默认限制 512-1024 层,太深会编译失败

  • 误以为 const int x = func();constexpr → 必须显式写 constexpr,否则仍是运行时计算

建议:

  • 所有 "可以编译期算" 的函数都加上 constexpr

  • 数组大小、模板参数、static_assert 首选 constexpr

  • C++17 后大量用 if constexpr 替换 SFINAE

  • 永远给 constexpr 函数加 noexcept,因为 constexpr 函数通常是纯函数,noexcept 告诉编译器它不会抛异常,有助于优化。

QA:

  • constexpr 和 const 区别?

const 只是 "只读",可以在运行时赋值;constexpr 必须编译期算出值,可以用在数组大小、模板参数等地方。

  • if constexpr 为什么比普通 if 好?

不满足的分支在编译期被完全丢弃,不会生成代码,不会实例化模板,编译更快、最终二进制更小。

  • constexpr 函数能有多复杂?

C++14 后几乎能写普通函数能写的一切(循环、局部变量),C++20 甚至能用 std::vector 等(但 11/14/17 还是有一定限制)。

  • 什么时候必须用 constexpr?

模板参数、数组大小、static_assert、编译期常量表。

  • constexpr 函数可以在运行时调用吗?

可以。如果传入的参数是运行时值,constexpr 函数会退化为普通函数在运行时执行;只有当传入编译期常量且上下文要求编译期结果时,才会在编译期计算:

cpp 复制代码
constexpr int add(int a, int b) { return a + b; }

int main() {
    int x = 1, y = 2;
    int z = add(x, y);       // 运行时计算(x、y 是运行时值)
    constexpr int w = add(3, 4); // 编译期计算(3、4 是常量)
}
相关推荐
人间打气筒(Ada)2 小时前
go:如何实现接口限流和降级?
开发语言·中间件·go·限流·etcd·配置中心·降级
云泽8082 小时前
深入红黑树:SGI-STL 中 map 与 set 的关联容器架构剖析
开发语言·c++·stl底层架构
REDcker2 小时前
C++ vcpkg:安装、使用、原理与选型
开发语言·c++·windows·操作系统·msvc·vcpkg
晓13132 小时前
React篇——第五章 React Router实战
开发语言·javascript·ecmascript
小陈工2 小时前
2026年3月30日技术资讯洞察:AI算力突破、云原生优化与架构理性回归
开发语言·人工智能·python·云原生·架构·数据挖掘·wasm
feng_you_ying_li2 小时前
set/map的封装,底层为红黑树.
c++
古城小栈2 小时前
Tonic:构建高性能 Rust gRPC 服务
开发语言·rust
晨非辰2 小时前
Git版本控制速成:提交三板斧/日志透视/远程同步15分钟精通,掌握历史回溯与多人协作安全模型
linux·运维·服务器·c++·人工智能·git·后端
我是大猴子2 小时前
JAVA面试问题
开发语言·python