C++14下内联函数使用constexpr变量的ODR违规判定问题
注:本问题仅针对C14标准,即C17引入inline变量特性之前的规则
- 内联函数定义中使用的constexpr变量,满足何种情况会被判定为odr-used,从而导致该函数的多定义违反[basic.def.odr]/6?
(相关规则大概率涉及[basic.def.odr]/3;如果在内联函数定义上下文内取这类constexpr变量的地址,是否会静默引入程序UB?)
示例问题:若doMath()定义如下:
// some_math.h #pragma once // 编码规范要求避免直接使用字面量 constexpr int kTwo{2}; inline int doMath(int arg) { return std::max(arg, kTwo); } // std::max(const int&, const int&)
当doMath()通过包含头文件的方式在两个不同翻译单元中定义并被使用时,程序是否存在未定义行为?
参考如下示例代码:
// constants.h #pragma once constexpr int kFoo{42}; // foo.h #pragma once #include "constants.h" inline int foo(int arg) { return arg * kFoo; } // #1: kFoo未被odr-used // a.cpp #include "foo.h" int a() { return foo(1); } // foo被odr-used // b.cpp #include "foo.h" int b() { return foo(2); } // foo被odr-used
上述代码基于C++14标准编译,该版本中尚未引入inline变量特性,因此constexpr变量不会被隐式标记为inline。
具有外部链接的内联函数foo在a.cpp和b.cpp对应的两个翻译单元TU_a、TU_b中均被odr-used,因此需要在两个TU中都提供定义(参考[basic.def.odr]/4)。
[basic.def.odr]/6规定了这类多翻译单元多定义场景的合法要求,其中核心相关条款为/6.1和/6.2:
程序中可以存在多份[...]具有外部链接的内联函数[...]的定义,前提是每个定义出现在不同翻译单元中,且所有定义满足以下要求:若名为D的实体在多个翻译单元中定义,则:
- /6.1 D的每个定义由相同的标记序列构成;
- /6.2 D的每个定义中,按[basic.lookup]规则查找到的对应名称,在重载决议([over.match])和偏特化匹配([temp.over])后,应指向D定义内的实体,或指向同一个实体;例外情况:名称可以指向具有内部或无链接的非易失性const对象,前提是该对象在D的所有定义中字面类型相同、由常量表达式([expr.const])初始化、未被odr-used、且在D的所有定义中值相同;
- ...
若D的定义不满足上述要求,则行为未定义。
上述示例中/6.1要求已满足。
/6.2的例外条款要求kFoo满足以下条件才能合规:
- [满足] 是具有内部链接的const对象
- [满足] 由常量表达式初始化
- [满足] 在
foo的所有定义中字面类型一致 - [满足] 在
foo的所有定义中值一致 - [待确认] 未被odr-used。
此处第5条应解读为“在foo的定义内未被odr-used”,标准措辞可以更明确。若kFoo(至少在foo定义内)被判定为odr-used,则违反[basic.def.odr]/6,触发ODR违规,导致程序未定义行为。
根据[basic.def.odr]/3的规则判定kFoo是否被odr-used:
变量x的名称出现在潜在求值表达式ex中时,x被ex odr-used,除非对x应用左值到右值转换([conv.lval])可以得到不调用任何非平凡函数的常量表达式([expr.const]);且如果x是对象,ex是表达式e的潜在结果集合的元素,其中e要么被应用了左值到右值转换([conv.lval]),要么是弃值表达式([expr]条款)。[...]
当前疑问点:如果在foo的定义内取kFoo的地址,或者在foo定义外取其地址,是否会影响[basic.def.odr]/6.2的合规性?
若foo按如下方式定义:
// #2 inline int foo(int arg) { std::cout << "&kFoo in foo() = " << &kFoo << "\n"; return arg * kFoo; }
a()和b()定义如下:
int a() { std::cout << "TU_a, &kFoo = " << &kFoo << "\n"; return foo(1); } int b() { std::cout << "TU_b, &kFoo = " << &kFoo << "\n"; return foo(2); }
依次调用a()和b()得到的输出如下:
TU_a, &kFoo = 0x401db8 &kFoo in foo() = 0x401db8 // <-- TU_a中的foo()取到TU_a的kFoo地址 TU_b, &kFoo = 0x401dbc &kFoo in foo() = 0x401db8 // <-- TU_b中的foo()意外取到TU_a的kFoo地址
即在非内联函数a()、b()中访问kFoo时,得到的是对应TU本地的kFoo地址,但在内联函数foo()中访问时,得到的是同一个kFoo地址。
按本部分定义的foo、a、b编写的程序是否存在未定义行为?
实际开发中的常见场景:用constexpr变量定义数学常量,在内联函数定义内将其作为参数传递给std::max()这类按引用传参的工具函数。
内容的提问来源于stack exchange,提问作者dfrib

