constexpr函数

一、核心作用

constexpr 是 C++11 引入的关键字,用于修饰函数(也可修饰变量),表示该函数支持编译期求值:当传入编译期常量作为参数时,可在编译阶段计算出结果,其返回值可作为常量表达式使用(如数组长度、模板非类型参数、枚举初始化等);当传入运行期变量时,也可作为普通函数在运行时执行。


二、C++11 对 constexpr 函数的严格语法要求

C++11 是 constexpr 的首个版本,对函数的约束非常严苛,核心要求如下:

1. 类型约束
  • 返回类型必须是字面类型(LiteralType) ,且不能是 void
  • 所有形参的类型也必须是字面类型

字面类型包括:算术类型(int/double 等)、枚举类型、指针 / 引用类型,以及满足条件的类类型(拥有 constexpr 构造函数、无虚函数、析构函数平凡)。

2. 函数体的强限制

C++11 要求 constexpr 函数体逻辑极度简单,函数体内只能包含以下内容

  • 空语句、static_assert 静态断言
  • typedef / using 类型别名声明
  • 恰好一条 return 语句

绝对禁止if/switch 分支语句、for/while 循环语句、局部变量定义、赋值操作、goto 等控制流或副作用语句。

注意:return 后的表达式中可以使用三元运算符 ?:、逻辑运算符、递归调用自身 ------ 这些属于表达式而非语句,是合法的。

3. 其他约束
  • 函数不能是虚函数。
  • 非静态成员函数被 constexpr 修饰时,隐含为 const 成员函数,不能修改类的成员变量。
  • constexpr 函数默认是 inline 的,通常定义在头文件中,以保证编译期可见。
  • 函数不能包含副作用(如修改全局变量、执行 I/O 操作等),编译期求值无法产生运行期副作用。

三、典型使用示例

1. 基础算术函数

cpp

运行

cpp 复制代码
// C++11 合法:仅一条 return 语句
constexpr int square(int x) {
    return x * x;
}

// 编译期使用:作为数组长度
int arr[square(5)]; // 等价于 int arr[25];

// 作为模板非类型参数
template<int N> struct Test {};
Test<square(4)> t; // 实例化为 Test<16>
2. 递归实现(允许递归调用)

C++11 支持 constexpr 函数递归,可通过递归 + 三元运算符实现循环逻辑:

cpp

运行

cpp 复制代码
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

constexpr int res = factorial(5); // 编译期计算得到 120
3. 类的 constexpr 成员函数

cpp

运行

cpp 复制代码
class Point {
    int x, y;
public:
    // constexpr 构造函数
    constexpr Point(int x_, int y_) : x(x_), y(y_) {}
    
    // constexpr 成员函数,隐含 const 属性
    constexpr int getX() const { return x; }
    constexpr int getY() const { return y; }
};

constexpr Point p(10, 20);
constexpr int px = p.getX(); // 编译期获取值 10

四、关键特性与注意事项

  1. 编译期 / 运行期双模式

    • 实参为编译期常量时,函数在编译期求值,结果为常量表达式;
    • 实参为运行期变量时,函数退化为普通函数,在运行时执行。

    cpp

    运行

    复制代码
    int a = 10;
    int b = square(a); // 运行时计算,完全合法
  2. 不强制编译期求值 constexpr 仅表示 "具备编译期求值能力",而非 "必须在编译期执行"。只有当结果被用于常量表达式语境(数组长度、模板参数等)时,编译器才会强制编译期计算。

  3. C++11 的局限性 C++11 的 constexpr 函数能力非常受限,无法实现复杂的多分支、循环逻辑。C++14 大幅放松了限制,允许局部变量、分支 / 循环语句、void 返回类型等。


五、C++11 中不合法的示例

cpp

运行

cpp 复制代码
// 错误:存在 if 语句(控制流语句不允许)
constexpr int abs(int x) {
    if (x < 0) return -x;
    return x;
}

// 错误:存在局部变量和 for 循环
constexpr int sum(int n) {
    int s = 0;
    for (int i = 1; i <= n; ++i) s += i;
    return s;
}

// 错误:返回 void 类型
constexpr void func() {}

从 C++11 首次引入 constexpr 开始,后续每一个标准版本都在持续放宽限制、扩展能力:从最初 "只能写一条 return" 的极简编译期函数,逐步演进到 C++20/23 接近通用的编译期计算体系。下面按版本顺序梳理核心更新与使用方式。


一、C++14:大幅放松函数体限制

C++14 是 constexpr 第一个重要扩展版本,核心是放开函数体的语句限制,让编译期函数可以编写常规逻辑代码。

核心放宽规则

  1. 函数体允许多条语句与常规控制流 不再强制只能有一条 return,支持:

    • 局部变量定义(必须是字面类型,且必须带初始化,不允许未初始化变量)
    • if / switch 分支语句
    • for / while / do-while 循环语句
    • 赋值、自增 / 自减等修改操作
    • 仍然禁止:gototry-catch、动态内存分配、I/O 操作、静态变量等。
  2. 允许 void 返回类型 constexpr 函数可以返回 void,用于编写编译期执行的过程式逻辑(如交换、赋值操作)。

  3. 取消成员函数的隐含 const C++11 中 constexpr 成员函数默认是 const 的,无法修改成员变量;C++14 起取消该限制,非 constconstexpr 成员函数可以修改对象状态(只要对象在编译期可修改)。

代码示例

cpp

运行

cpp 复制代码
// 1. 分支语句:C++11 非法,C++14 合法
constexpr int abs(int x) {
    if (x < 0)
        return -x;
    return x;
}

// 2. 循环 + 局部变量:C++11 非法,C++14 合法
constexpr int sum(int n) {
    int s = 0;
    for (int i = 1; i <= n; ++i) {
        s += i;
    }
    return s;
}

// 3. void 返回的 constexpr 函数
constexpr void swap(int& a, int& b) {
    int tmp = a;
    a = b;
    b = tmp;
}

// 4. 可修改成员的 constexpr 成员函数
class Counter {
    int val;
public:
    constexpr Counter(int v) : val(v) {}
    constexpr void inc() { val++; } // 修改成员
    constexpr int get() const { return val; }
};

constexpr Counter build_counter() {
    Counter c(0);
    c.inc();
    c.inc();
    return c;
}
constexpr auto c = build_counter(); // 编译期构造+修改,c.get() == 2

二、C++17:扩展适用场景 + constexpr lambda

C++17 重点扩展 constexpr 的应用范围,解决工程化问题,并引入 lambda 支持。

核心更新

  1. constexpr lambda 表达式 lambda 可以显式声明为 constexpr;如果 lambda 的函数体天然满足 constexpr 函数要求,即使不写关键字,其 operator() 也会默认是 constexpr 的。

    cpp

    运行

    cpp 复制代码
    // 显式 constexpr lambda
    constexpr auto square = [](int x) { return x * x; };
    constexpr int res1 = square(5); // 编译期计算 = 25
    
    // 隐式 constexpr lambda(满足条件自动支持)
    auto add = [](int a, int b) { return a + b; };
    constexpr int res2 = add(3, 4); // 编译期计算 = 7
  2. constexpr 变量默认 inline 命名空间作用域的 constexpr 变量默认具备 inline 属性,可以直接定义在头文件中,被多个翻译单元包含也不会出现「多重定义」链接错误,彻底解决了 C++11/14 中头文件 constexpr 变量的链接问题。

  3. if constexpr(编译期分支) 配合模板元编程使用,可以在编译期根据条件丢弃不选中的代码分支,常与 constexpr 函数结合实现编译期逻辑分发。

    cpp

    运行

    cpp 复制代码
    #include <type_traits>
    template<typename T>
    constexpr T double_value(T t) {
        if constexpr (std::is_integral_v<T>) {
            return t * 2;
        } else {
            return t;
        }
    }
  4. 其他补充

    • 放宽字面类型限制,更多自定义类可以作为字面类型用于编译期;
    • 标准库大规模 constexpr 化:std::arraystd::string_view、多数算法等开始支持编译期调用。

三、C++20:里程碑式升级 ------ 接近通用编译期计算

C++20 是 constexpr 能力的质变版本,打破了绝大多数核心限制,编译期计算能力接近运行时,同时新增两个互补关键字完善语义。

1. 语言层面的核心放宽

  • 允许编译期动态内存分配 编译期可以使用 new / delete 分配与释放内存,要求分配的内存必须在编译期全部释放,不能逃逸到运行时 (即不能将编译期分配的指针返回给运行期使用)。基于此,std::stringstd::vector 等标准容器开始支持 constexpr

  • 允许 constexpr 虚函数与编译期多态 虚函数可以声明为 constexpr,支持编译期的虚函数调用与多态行为。

  • 允许 constexpr 析构函数 类的析构函数可以声明为 constexpr,字面类型不再强制要求析构函数是平凡的。

  • 允许 try-catch 块 constexpr 函数中可以编写 try-catch 结构,但仍禁止 throw 表达式,不能主动抛出异常,仅支持结构化的错误捕获框架。

  • 仍然禁止的内容:goto 语句、静态存储期变量、线程局部变量、I/O 操作、内联汇编等。

2. 新增三个配套关键字

C++20 引入 constevalconstinit,和 constexpr 形成互补,明确不同的编译期语义:

表格

关键字 语义 典型使用场景
constexpr 函数 / 变量:既可编译期也可运行期 通用编译期计算,双模式兼容
consteval 函数:强制编译期求值,运行时不可调用 必须在编译期完成计算的场景
constinit 变量:强制静态初始化,变量本身可修改 解决全局变量初始化顺序问题

示例:

cpp

运行

cpp 复制代码
// consteval:只能编译期调用
consteval int factorial(int n) {
    int res = 1;
    for (int i = 2; i <= n; ++i) res *= i;
    return res;
}
constexpr int v1 = factorial(5); // 正确:编译期计算
int x = 5;
int v2 = factorial(x); // 错误:x 不是编译期常量

// constinit:保证静态初始化,变量可修改
constinit int global_counter = 0;

3. 编译期容器示例(C++20)

cpp

运行

cpp 复制代码
#include <vector>
#include <numeric>

constexpr int sum_1_to_n(int n) {
    std::vector<int> nums;
    for (int i = 1; i <= n; ++i) {
        nums.push_back(i);
    }
    return std::accumulate(nums.begin(), nums.end(), 0);
}

constexpr int result = sum_1_to_n(10); // 编译期计算得到 55

四、C++23:细节补全与易用性提升

C++23 在 C++20 的基础上补全能力边界,让编译期编程更贴近常规代码写法。

核心更新

  1. 允许 throw 表达式与完整异常处理consteval 函数中允许使用 throw 抛出异常,且可以被 catch 捕获;若异常未在编译期捕获,则直接触发编译错误。编译期错误处理能力与运行时对齐。

  2. if consteval:区分编译期 / 运行期路径constexpr 函数内部判断当前是否处于编译期求值上下文,从而为编译期和运行期分别提供不同实现。

    cpp

    运行

    cpp 复制代码
    #include <iostream>
    constexpr int compute(int x) {
        if consteval {
            return x * 2; // 编译期分支:纯计算
        } else {
            std::cout << "runtime call\n"; // 运行期分支:可包含副作用
            return x * 2;
        }
    }
    
    constexpr int a = compute(10); // 走编译期分支
    int b = compute(20); // 走运行期分支,打印输出
  3. 其他扩展

    • 支持 consteval lambda 表达式;
    • 允许 constexpr 联合体、位域操作;
    • 标准库进一步 constexpr 化,绝大多数算法和容器都支持编译期使用。

五、版本演进总览

表格

核心特性 C++11 C++14 C++17 C++20 C++23
单 return 语句
分支 / 循环 / 局部变量
void 返回值
constexpr lambda
动态内存分配
虚函数 / 析构函数
consteval / constinit
throw 异常
if consteval

cpp 复制代码
constexpr int sum(int n) {
    int s = 0;
    for (int i = 1; i <= n; ++i) s += i;
    return s;
}

// 错误:返回 void 类型
constexpr void func() {}
相关推荐
凡人叶枫3 小时前
Effective C++ 条款41:了解隐式接口和编译期多态
java·开发语言·c++·effective c++
凡人叶枫3 小时前
Effective C++ 条款42:了解 typename 的双重意义
java·linux·服务器·c++
小胖xiaopangss3 小时前
BRpc使用
c++·rpc
-森屿安年-4 小时前
63. 不同路径 II
c++·算法·动态规划
chase_my_dream4 小时前
Cartographer详细讲解
c++·人工智能·自动驾驶
森G4 小时前
75、服务器源码解析---------云视频服务项目
linux·服务器·网络·c++·qt
碧海蓝天20224 小时前
C++法则24:在标准 C++ 中,没有任何可移植的方式判断指针 T* pt 指向的内存位置是否已经 构造了对象,程序员必须手动跟踪哪些元素已构造。
java·开发语言·c++
charlie1145141914 小时前
现代C++指南:Lambda,让我们用另一种方式持有函数
开发语言·c++
森G5 小时前
77、线程池原理和实现------服务器源码解析----云视频服务项目
服务器·c++·qt