GCC在-Og/-O1模式下传递constexpr成员为引用触发链接错误
问题复现代码
#include <stdio.h> class Foo { public: static constexpr double d = 10.0; static constexpr int i = 10; }; //class Foo void print_double_ref(const double& v) { printf("v = %g\n", v); } void print_int_ref(const int& n) { printf("n = %i\n", n); } int main() { Foo a; //This part triggers link error with -Og, -O1 //No error with -O2, -O3 print_double_ref(a.d); print_int_ref(a.i); //This part does not trigger link error with -Og, -O1 print_int_ref(a.d); print_double_ref(a.i); }
现象说明
上述代码在GCC的-Og或-O1优化模式下编译时,会触发*undefined reference to Foo::d和undefined reference to Foo::i*的链接错误;但在-O2或-O3模式下无此问题。此外,跨类型传递constexpr成员作为引用参数(即最后两次调用print_int_ref(a.d)和print_double_ref(a.i))时,不会触发错误。
疑问点
- 为何仅在
-Og和-O1模式下出现链接错误? - 为何跨类型传递constexpr成员作为引用参数时不会产生错误?
- 已知
static const成员需在类外初始化,但constexpr变量必须在声明点定义,为何此类结构可通过编译却触发链接错误?
问题解答
1. 仅低优化级别触发链接错误的原因
C++标准中,static constexpr成员变量在类内声明时已经完成了定义,但存在例外情况:当代码需要该变量的内存地址(比如绑定到引用)时,必须满足*ODR-used(单一定义规则)*要求,即变量要有实际的内存实例。
在-O2/-O3等高优化级别下,GCC会直接将constexpr成员的常量值内联替换到调用点,完全不需要访问变量的内存地址,因此不会触发ODR使用要求,也就不需要链接变量的实际定义。
而在-Og/-O1这类低优化级别下,编译器不会进行激进的内联替换,会尝试生成对Foo::d和Foo::i内存地址的引用。此时因为类内的constexpr声明没有在类外提供全局定义(虽然标准允许类内定义,但GCC低优化下会严格要求显式外部定义),链接器找不到对应符号,就会抛出未定义引用错误。
2. 跨类型传递无错误的原因
当把constexpr double传递给const int&参数,或者反过来时,编译器会先创建一个临时变量:比如print_int_ref(a.d)会生成值为10的临时int变量,再将这个临时变量的引用传递给函数。
此时代码引用的是临时变量的地址,而非Foo::d或Foo::i的内存地址,不会触发对Foo类静态成员的ODR使用要求,自然不会出现链接错误。
3. constexpr成员类内定义为何仍触发链接错误
虽然C++11及以后标准规定static constexpr成员在类内声明时即完成定义,但编译器处理ODR使用场景(比如绑定到引用)时,可能需要变量有外部定义实例。GCC在低优化级别下会严格检查这种场景:即使类内已有constexpr定义,当需要取地址或绑定引用时,仍要求在类外显式提供定义(比如添加constexpr double Foo::d;和constexpr int Foo::i;),否则链接器无法找到对应符号。
而高优化级别下,编译器通过内联规避了对变量地址的需求,无需外部定义也能正常链接。
内容的提问来源于stack exchange,提问作者one_two_three

