在 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) 满足以下条件:
- 对相同实参总返回相同结果;
- 无副作用(不修改全局或局部静态变量,不执行 I/O 等)。
纯函数应优先声明为
constexpr
。
(补充) C++23 起,constexpr 函数可读写静态局部变量;由于静态变量跨调用保持状态,这已构成副作用,但标准仍允许。
最佳实践 除非有明确理由,否则凡符合常量表达式要求的函数均应声明为 constexpr,即便当前仅在运行期调用。
为什么不把所有函数都写成 constexpr?
constexpr
是接口承诺:若函数无法满足常量表达式需求,就不应标记为constexpr
。- 一旦标记为
constexpr
,后续移除将破坏依赖此承诺的代码。 - constexpr 函数在调试器中无法下断点或单步,调试体验较差。
既然只在运行期调用,为什么还要 constexpr?
- 几乎没有额外成本,反而可能帮助编译器做更激进的优化(更小、更快)。
- 代码未来可能被复用或扩展;若将来需要在常量上下文调用却未提前标记
constexpr
,将不得不返工并重新测试。 - 重复实践最佳习惯,减少日后维护负担。