目前 无法 通过任何关键字向编译器“建议”:只要可行,就优先在编译期求值某个 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_evaluated
与 if 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 课《多模板类型函数模板》。