constexpr 函数(第三部分)与 consteval

目前 无法 通过任何关键字向编译器“建议”:只要可行,就优先在编译期求值某个 constexpr 函数。唯一可靠的做法是:把返回值用在需要常量表达式的上下文中,从而迫使编译期求值;但这需要逐调用地手动处理。

最常见的手法是用返回值初始化一个 constexpr 变量(我们在前面示例中一直使用变量 g)。然而,这种做法必须额外引入一个变量,代码显得累赘且可读性下降。

(高级读者) 社区曾提出过若干“奇技淫巧”来避免每次都声明 constexpr 变量,详见 此处此处

从 C++20 开始,这一问题有了更优雅的解决方案,下文即将介绍。

C++20 的 consteval

C++20 引入关键字 consteval,用于声明立即函数(immediate function):此类函数必须在编译期求值,否则产生编译错误。

#include <iostream>

consteval int greater(int x, int y)   // 立即函数
{
    return (x > y ? x : y);
}

int main()
{
    constexpr int g{ greater(5, 6) };              // OK:编译期求值
    std::cout << g << '\n';

    std::cout << greater(5, 6) << " is greater!\n"; // OK:编译期求值

    int x{ 5 };                                    // 非 constexpr
    std::cout << greater(x, 6) << " is greater!\n"; // 错误:不能在运行期调用
    return 0;
}

前两处调用均满足编译期求值要求;第三处因实参 x 非编译期常量,导致编译失败。

最佳实践 若某段逻辑必须在编译期完成(例如只能编译期执行的操作),请使用 consteval

值得注意的是,consteval 函数的形参并非 constexpr,这是为了与既有规则保持一致:函数仍可在编译期被调用,但形参本身并不隐含常量表达式属性。

判断一次 constexpr 函数调用在编译期还是运行期执行

C++ 目前没有可靠机制来直接探知某次 constexpr 函数调用究竟发生在编译期还是运行期。

std::is_constant_evaluatedif consteval(高级话题)

  • std::is_constant_evaluated()(定义于 <type_traits>)返回 bool,指示当前是否处于常量求值上下文(constant-evaluated context),即标准中要求常量表达式的场景(如 constexpr 变量初始化)。
  • 设计初衷是允许在函数内部按求值环境选择不同实现:
#include <type_traits>

constexpr int someFunction()
{
    if (std::is_constant_evaluated())   // 若处于常量求值上下文
        return doSomething();           // 编译期路径
    else
        return doSomethingElse();       // 运行期路径
}

然而,编译器也可能在非强制上下文中自愿编译期求值;此时 std::is_constant_evaluated() 仍返回 false,尽管实际已编译期执行。因此,它真正表达的是“标准强制编译期求值”,而非“实际是否编译期求值”。

关键要点 标准本身并不区分“编译期”与“运行期”;若 std::is_constant_evaluated() 在任意编译期求值场景都返回 true,优化器改为编译期求值就可能改变可观察行为,从而违反“优化不改变行为”原则。

C++23 引入的 if consteval 提供了更简洁的语法并修正了部分问题,但其判定逻辑与 std::is_constant_evaluated() 相同。

借助 consteval 强制 constexpr 编译期求值(C++20)

consteval 函数无法在运行期求值,灵活性低于 constexpr。我们仍希望有一种便捷手段能在必要时强制 constexpr 函数编译期求值,而在不可能时回退到运行期。

以下示例展示了可行做法:

#include <iostream>

// C++20 版:立即调用的 consteval lambda(Jan Schultke)
#define CONSTEVAL(...) [] consteval { return __VA_ARGS__; }()

// C++11 版:借助 constexpr 变量(Justin)
#define CONSTEVAL11(...) [] { constexpr auto _ = __VA_ARGS__; return _; }()

constexpr int compare(int x, int y)
{
    if (std::is_constant_evaluated())
        return (x > y ? x : y);   // 编译期路径
    else
        return (x < y ? x : y);   // 运行期路径
}

int main()
{
    int x{ 5 };
    std::cout << compare(x, 6) << '\n';           // 运行期,输出 5
    std::cout << compare(5, 6) << '\n';           // 可能运行期,输出 5
    std::cout << CONSTEVAL(compare(5, 6)) << '\n'; // 强制编译期,输出 6
    return 0;
}

(高级读者) 上述代码利用可变参宏立即调用的 consteval lambda 实现;宏与 lambda 相关内容分别见 宏替换 与 20.6 课《Lambda 表达式》。

GCC 用户注意:GCC 14 起存在优化缺陷,下列无宏版本在开启任何级别优化时可能给出错误结果:

#include <iostream>

consteval auto CONSTEVAL(auto value) { return value; }

int main()
{
    std::cout << CONSTEVAL(compare(5, 6)) << '\n'; // 强制编译期求值
    return 0;
}

由于 consteval 函数的实参始终处于显式常量求值上下文,当把 constexpr 函数作为实参传入时,该 constexpr 函数必须编译期求值;随后 consteval 函数把结果返回给调用者。 注意:返回方式为按值返回。在编译期上下文中,整个调用会被结果替换,因此即使返回类型复制成本较高(如 std::string)也无关紧要。

(高级读者)

  • auto 返回类型见 10.9 课《函数类型推导》。
  • 简写函数模板(auto 形参)见 11.8 课《多模板类型函数模板》。

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

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

公众号二维码

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