constexpr 函数(第四部分)

在 constexpr 或 consteval 函数体内,可以声明并修改既非 const 也非 constexpr 的局部变量,其值在函数内部可变。

示例:

#include <iostream>

consteval int doSomething(int x, int y)
{
    x = x + 2;           // 可修改形参的值

    int z{ x + y };      // 可声明非常量局部变量
    if (x > y)
        z = z - 1;       // 并可再次修改其值

    return z;
}

int main()
{
    constexpr int g{ doSomething(5, 6) };
    std::cout << g << '\n';
    return 0;
}

当上述函数在编译期求值时,编译器会模拟执行函数并返回计算结果。

constexpr / consteval 函数可用形参或局部变量作为 constexpr 函数调用的实参

前文提到:“当 constexpr(或 consteval)函数在编译期求值时,其调用的所有函数也必须编译期求值。” 然而令人意外的是,即使形参本身并非 constexpr,或局部变量并非 const,只要整个 constexpr 函数正处于编译期求值,编译器就已知晓这些变量在调用点的具体值,因此允许将它们作为实参传给 constexpr 函数,并仍可在编译期完成该调用。

示例:

#include <iostream>

constexpr int goo(int c)     // goo 为 constexpr
{
    return c;
}

constexpr int foo(int b)     // b 在 foo 内不是常量表达式
{
    return goo(b);           // 若 foo 编译期求值,则 goo(b) 亦可编译期求值
}

int main()
{
    std::cout << foo(5);
    return 0;
}
  • foo(5) 编译期求值,编译器已知 b == 5,于是把 goo(b) 视为 goo(5) 并继续编译期求值。
  • foo(5) 运行期求值,则 goo(b) 亦运行期求值。

constexpr 函数能否调用非 constexpr 函数?

可以,但仅当该 constexpr 函数处于非强制常量表达式上下文时方可调用非 constexpr 函数。 若 constexpr 函数必须在常量表达式中求值,则其内部任何对非 constexpr 函数的调用都将导致编译错误。

允许在非强制上下文中调用非 constexpr 函数的典型用途:

#include <type_traits>

constexpr int someFunction()
{
    if (std::is_constant_evaluated())
        return someConstexprFcn();
    else
        return someNonConstexprFcn();
}

再看另一种写法:

constexpr int someFunction(bool b)
{
    if (b)
        return someConstexprFcn();
    else
        return someNonConstexprFcn();
}

只要 someFunction(false) 永远不被用在常量表达式中即可通过编译。

(历史说明) C++23 之前,标准规定 constexpr 函数必须“至少存在一组实参使其返回常量表达式”,否则为病态程序(ill-formed);但编译器可静默放过。C++23 撤销了该要求。

建议:

  • 如非必要,避免在 constexpr 函数内部调用非 constexpr 函数。
  • 若需区分常量/非常量上下文,使用 if (std::is_constant_evaluated())(C++20)或 if consteval(C++23 起)。
  • 始终在常量表达式上下文中测试你的 constexpr 函数,以防“运行期可用、编译期失败”。

何时应将函数声明为 constexpr?

通则:若某函数在任何需要常量表达式的场景下都能正确求值,则应将其声明为 constexpr

纯函数(pure function) 满足以下条件:

  1. 对相同实参总返回相同结果;
  2. 无副作用(不修改全局或局部静态变量,不执行 I/O 等)。 纯函数应优先声明为 constexpr

(补充) C++23 起,constexpr 函数可读写静态局部变量;由于静态变量跨调用保持状态,这已构成副作用,但标准仍允许。

最佳实践 除非有明确理由,否则凡符合常量表达式要求的函数均应声明为 constexpr,即便当前仅在运行期调用。

为什么不把所有函数都写成 constexpr?

  1. constexpr 是接口承诺:若函数无法满足常量表达式需求,就不应标记为 constexpr
  2. 一旦标记为 constexpr,后续移除将破坏依赖此承诺的代码。
  3. constexpr 函数在调试器中无法下断点或单步,调试体验较差。

既然只在运行期调用,为什么还要 constexpr?

  • 几乎没有额外成本,反而可能帮助编译器做更激进的优化(更小、更快)。
  • 代码未来可能被复用或扩展;若将来需要在常量上下文调用却未提前标记 constexpr,将不得不返工并重新测试。
  • 重复实践最佳习惯,减少日后维护负担。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"