你可能认为只要函数被声明为 constexpr
,就总能在编译期求值;事实并非如此。
在《常量表达式》中我们指出:若上下文不要求常量表达式,编译器可自行决定在编译期或运行期求值。因此,任何属于“非强制常量表达式”的 constexpr
函数调用,既可能编译期求值,也可能运行期求值。
示例:
#include <iostream>
constexpr int getValue(int x)
{
return x;
}
int main()
{
int x{ getValue(5) }; // 可在运行期或编译期求值
return 0;
}
此处 getValue()
虽为 constexpr
,但其调用位于非强制上下文中(变量 x
不是 constexpr
),故编译器可自由选择求值时机。
关键要点
constexpr
函数的编译期求值仅在需要常量表达式时才得到保证。
强制常量表达式中 constexpr 函数的诊断
编译器并不强制在函数定义阶段就验证其是否一定可在编译期求值;只有在真正需要编译期求值的上下文中,才会进行完整检查。
因此,很容易写出对运行期使用完全合法,却在编译期求值时报错的 constexpr
函数。
举一示例:
#include <iostream>
int getValue(int x) // 普通函数,非 constexpr
{
return x;
}
// 运行期可正常调用
// 若以编译期方式调用,则因调用非 constexpr 函数 getValue(x) 而报错
constexpr int foo(int x)
{
if (x < 0) return 0; // C++23 前需此行,见文末说明
return getValue(x); // 此处调用非 constexpr 函数
}
int main()
{
int x{ foo(5) }; // 合法:运行期求值
constexpr int y{ foo(5) }; // 编译错误:无法在编译期求值
return 0;
}
当 foo(5)
用于初始化非 constexpr
变量 x
时,它在运行期求值,一切正常。
当用于初始化 constexpr
变量 y
时,则必须编译期求值;此时编译器发现 getValue()
非 constexpr
,于是报错。
因此,编写 constexpr
函数后,务必在强制常量表达式场景(如初始化 constexpr
变量)中显式测试其可编译性。
最佳实践
所有 constexpr
函数都应确保能够在编译期求值,因为在强制常量表达式上下文中这是必需的。
务必在需要常量表达式的上下文中测试你的 constexpr
函数,以免出现“运行期可用、编译期不可用”的情况。
(高级说明)
在 C++23 之前,若不存在任何参数组合能使 constexpr
函数在编译期求值,则程序为病态(ill-formed),且无需诊断。上例若去掉 if (x < 0) return 0;
,即没有任何参数可使函数在编译期成功,程序即属病态。C++23(P2448R1)已撤销此要求。
constexpr / consteval 函数形参并非 constexpr
constexpr
函数的形参不是隐式 constexpr
,亦不可显式声明为 constexpr
。
关键要点
若形参为 constexpr
,则意味着只能以 constexpr
实参调用该函数;事实并非如此——constexpr
函数在运行期求值时,完全可以接受非 constexpr
实参。
因此,这些形参不能用于函数体内的常量表达式。
consteval int goo(int c) // c 不是 constexpr,不能在常量表达式中使用
{
return c;
}
constexpr int foo(int b) // b 不是 constexpr,不能在常量表达式中使用
{
constexpr int b2{ b }; // 错误:constexpr 变量需常量表达式初始化
return goo(b); // 错误:consteval 函数需常量表达式实参
}
int main()
{
constexpr int a{ 5 };
std::cout << foo(a); // 合法:常量表达式 a 可作为实参
return 0;
}
形参可声明为 const
,此时其为运行期常量。
相关内容 若需“常量表达式形参”,请参见 11.9 课《非类型模板形参》。
constexpr 函数隐式内联
当 constexpr
函数在编译期求值时,编译器必须在使用点之前看到其完整定义(仅前向声明不足)。若该函数在多个源文件中使用,则每个翻译单元都需包含其定义,否则违反一次定义规则(ODR)。
为此,constexpr
函数被隐式声明为 inline
,从而免受 ODR 限制。因此,constexpr
函数通常置于头文件中,以供各 .cpp
文件包含。
规则
编译器必须看到 constexpr
(或 consteval
)函数的完整定义,而不仅是前向声明。
最佳实践
- 仅在单个源文件中使用的
constexpr
/consteval
函数,应在该文件中且在使用前定义。 - 在多个源文件中使用的
constexpr
/consteval
函数,应定义于头文件,以便包含。
若某 constexpr
函数仅在运行期调用,则前向声明即可满足编译器需求;此时可在其他翻译单元中定义其实现。
(高级说明) 根据 CWG2166,当且仅当“在导致调用的最外层求值发生之前”该函数已定义即可。因此,以下代码合法:
constexpr int foo(int);
constexpr int goo(int c)
{
return foo(c); // foo 尚未定义
}
constexpr int foo(int b)
{
return b; // 在 goo 被求值前已定义
}
int main()
{
constexpr int a{ goo(5) }; // 最外层求值点
return 0;
}
此举旨在支持相互递归的 constexpr
函数。
再举一例
通过下例进一步说明 constexpr
函数的求值时机:
#include <iostream>
constexpr int greater(int x, int y)
{
return (x > y ? x : y);
}
int main()
{
constexpr int g{ greater(5, 6) }; // 情况 1:强制编译期求值
std::cout << g << " is greater!\n";
std::cout << greater(5, 6) << " is greater!\n"; // 情况 2:可编译期或运行期
int x{ 5 }; // 非 constexpr,但值在编译期可知
std::cout << greater(x, 6) << " is greater!\n"; // 情况 3:通常运行期
std::cin >> x;
std::cout << greater(x, 6) << " is greater!\n"; // 情况 4:必定运行期
return 0;
}
- 情况 1:调用位于强制常量表达式上下文,必编译期求值。
- 情况 2:上下文不要求常量表达式,实参为常量表达式,编译器可任选求值时机。
- 情况 3:实参非常量表达式,通常运行期求值。虽值在编译期已知,但
x
非constexpr
,故编译器“多半”运行期求值(as-if 规则下也可能优化)。 - 情况 4:
x
的值编译期不可知,必运行期求值。
关键要点 可将函数是否真正编译期求值的概率归纳如下:
必编译期(标准强制): • 在需要常量表达式的上下文中调用
constexpr
函数; • 在被其他已确定编译期求值的函数内部调用。极可能(无理由不): • 不要求常量表达式,但所有实参均为常量表达式。
可能(as-if 规则优化): • 不要求常量表达式,部分实参虽非常量表达式但其值编译期可知; • 非
constexpr
函数,所有实参为常量表达式。必运行期(不可能编译期): • 存在实参值编译期不可知。
编译器的优化级别设置也会影响上述决策;调试版(通常关闭优化)与发布版可能表现不同。例如,GCC 与 Clang 若未开启优化(如 -O2
),在不要求常量表达式的上下文中不会把 constexpr
函数编译期求值。
(高级说明) 编译器也可能内联或完全消除某函数调用,从而影响求值时机或是否真正发生求值。