F-4 Constexpr 函数(第四部分)

constexpr/consteval 函数可使用非常量局部变量

在 constexpr 或 consteval 函数内部,我们可以使用非 constexpr 的局部变量,且这些变量的值可以被修改。

举个简单的例子:

#include <iostream>

consteval int doSomething(int x, int y) // function is consteval
{
    x = x + 2;       // we can modify the value of non-const function parameters

    int z { x + y }; // we can instantiate non-const local variables
    if (x > y)
        z = z - 1;   // and then modify their values

    return z;
}

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

    return 0;
}

image

当此类函数在编译时被求值时,编译器会实质上“执行”该函数并返回计算结果。


constexpr/consteval 函数可在 constexpr 函数调用中使用函数形参和局部变量作为实参

如前所述:“当 constexpr(或 consteval)函数在编译时被求值时,其调用的任何其他函数都必须在编译时完成求值。”

令人意外的是,constexpr 或 consteval 函数可以将其函数形参(非 constexpr)甚至局部变量(可能完全非 const)作为 constexpr 函数调用的实参。当 constexpr 或 consteval 函数在编译时被求值时,所有函数参数和局部变量的值必须为编译器所知(否则无法在编译时完成求值)。因此在此特殊语境下,C++ 允许将这些值作为 constexpr 函数调用的实参,且该 constexpr 函数调用仍可在编译时完成求值。

#include <iostream>

constexpr int goo(int c) // goo() is now constexpr
{
    return c;
}

constexpr int foo(int b) // b is not a constant expression within foo()
{
    return goo(b);       // if foo() is resolved at compile-time, then `goo(b)` can also be resolved at compile-time
}

int main()
{
    std::cout << foo(5);

    return 0;
}

image

在上例中,foo(5) 可能在编译时被求值,也可能不会。若被求值,编译器便知 b 的值为 5。即使 b 不是 constexpr,编译器仍可将 goo(b) 的调用视为 goo(5) 并进行编译时求值。若 foo(5) 在运行时解析,则 goo(b) 也将在运行时解析。


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

答案是肯定的,但仅限于常量表达式函数在非常量上下文中被求值时。当常量表达式函数在常量上下文中求值时(此时常量表达式函数无法产生编译时常量值),禁止调用非常量表达式函数,否则将引发编译错误。

允许调用非常量表达式函数,是为了使常量表达式函数能够执行如下操作:

#include <type_traits> // for std::is_constant_evaluated

constexpr int someFunction()
{
    if (std::is_constant_evaluated()) // if evaluating in constant context
        return someConstexprFcn();
    else
        return someNonConstexprFcn();
}

现在考虑这个变体:

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

只要someFunction(false)从未在常量表达式中被调用,此写法即为合法。

顺带一提...
在C++23之前,C++标准规定constexpr函数必须至少有一组实参返回constexpr值,否则在技术上属于语法错误。在constexpr函数中无条件调用非constexpr函数会导致该constexpr函数成为语法错误。但编译器无需对此类情况生成错误或警告——因此除非在常量上下文中调用此类常量表达式函数,否则编译器通常不会报错。C++23中已废除该要求。

为获得最佳效果,建议采取以下做法:

  1. 尽可能避免在 constexpr 函数内部调用非 constexpr 函数。
  2. 若您的 constexpr 函数需要在常量上下文与非常量上下文中表现不同行为,请使用 if (std::is_constant_evaluated())(C++20)或 if consteval(C++23 及更高版本)进行条件判断。
  3. 始终在常量上下文中测试 constexpr 函数,因为它们在非常量上下文中调用时可能正常工作,但在常量上下文中却可能失败。

何时应将函数声明为 constexpr?

一般而言,若函数可作为必需常量表达式的一部分进行求值,则应将其声明为 constexpr。

纯函数pure function是指满足以下条件的函数:

  • 当给定相同实参时,该函数始终返回相同的返回结果
  • 该函数不产生副作用(例如:不改变静态局部或全局变量的值,不进行输入或输出等)。

纯函数通常应声明为constexpr。

顺带一提……
常量表达式函数不必总是纯函数。在C++23中,常量表达式函数可以使用并修改静态局部变量。由于静态局部变量的值在函数调用间保持不变,修改静态局部变量被视为副作用。

话虽如此,若你的程序很简单或属于一次性代码,即使未将函数声明为常量表达式,世界也不会因此终结。但愿如此。

最佳实践
除非存在特殊原因,否则凡是可作为常量表达式进行求值的函数,都应声明为 constexpr(即使当前尚未如此使用)。
无法作为必需常量表达式的一部分进行求值的函数不应标记为 constexpr。


为什么不是所有函数都标记为 constexpr?

不应将函数标记为 constexpr 的原因包括:

  1. constexpr 标记表示该函数可用于常量表达式中。若函数无法作为常量表达式的一部分进行求值,则不应标记为 constexpr。
  2. constexpr是函数接口的一部分。一旦函数被标记为constexpr,它就能被其他constexpr函数调用,或用于需要常量表达式的场景。后期移除constexpr标记将导致相关代码失效。
  3. constexpr函数的调试难度更高,因为调试器无法在其中设置断点或执行单步调试。

为什么要在函数实际未在编译时求值的情况下使用 constexpr 声明?

新手程序员有时会问:“为什么要在函数仅在运行时被调用(例如因函数调用实参为非 const)的情况下使用 constexpr 声明?”

原因有几点:

  1. 使用 constexpr 几乎没有弊端,反而可能帮助编译器优化程序,使其更小更快。
  2. 当前虽未在编译时可求值的上下文中调用该函数,但当你修改或扩展程序时,仍可能在该上下文中调用它。若未提前声明为 constexpr,待实际调用时可能忽略此操作,从而错失性能优化机会。或者当你需要在要求常量表达式的上下文中使用返回值时,可能被迫在后期添加 constexpr 标记。
  3. 重复实践有助于巩固最佳实践。

在非简单项目中,应以函数可能被复用(或扩展)的思维模式进行设计。每次修改现有函数都存在破坏功能的风险,这意味着需要重新测试,耗费时间精力。多花一两分钟“一次做好”往往更划算,避免日后重复修改(及测试)。

posted @ 2026-03-13 10:37  游翔  阅读(0)  评论(0)    收藏  举报