(注意!这个问题特别涉及C++14在C++17中引入内联变量之前的状态)
TLDR;问题
- 是什么构成了对内联函数定义中使用的constexpr变量的odr使用,使得函数的多个定义违反了[basic.def.odr]/6
(…很可能是[basic.def.odr]/3;但一旦在内联函数定义的上下文中获取了这样一个constexpr变量的地址,这会在程序中悄悄引入UB吗?)
TLDR示例:执行一个程序,其中doMath()
定义如下:
// some_math.h
#pragma once
// Forced by some guideline abhorring literals.
constexpr int kTwo{2};
inline int doMath(int arg) { return std::max(arg, kTwo); }
// std::max(const int&, const int&)
一旦在两个不同的翻译单元中定义了doMath()
(例如通过包含some_math.h
和随后使用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 not 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编译,特别是在内联变量之前,因此在constexpr变量隐式内联之前。
内联函数foo
(具有外部链接)在与a.cpp
和b.cpp
相关的两个翻译单元(TU)中使用,例如TU_a
和TU_b
,因此应在这两个TU中定义([basic.def.odr]/4)
[basic.def.odr]/6涵盖了何时可能出现此类多个定义(不同的TU)的要求,特别是/6.1和/6.2与此相关[emphasismine]:
程序中可以有多个具有外部链接[…]的[…]内联函数的定义假设每个定义出现在不同的翻译单元中,并且只要这些定义满足以下要求。鉴于这样一个名为D的实体在多个翻译单元中定义,然后是
/6.1 D的每个定义应由相同的令牌序列组成;和
/6.2在D的每个定义中,根据【basic.lookup】查找的相应名称应指在D的定义,或应指过载后的同一实体分辨率([over.match])和部分模板匹配后specialization([temp.over]),除了名称可以引用具有内部链接或没有链接的非易失性常量对象在D的所有定义中具有相同的文字类型,并且对象为使用常量表达式([expr.const])初始化,并对象未使用odr,并且该对象在所有定义中都具有相同的值D;和
。。。
如果D的定义不满足这些要求,则行为是未定义的
/6.1已实现。
/6.2如果满足foo
:中的kFoo
- [OK]与内部链接常量
- [OK]使用常量表达式初始化
- [OK]在
foo
的所有定义中都是相同的文字类型 - [OK]在
foo
的所有定义中都具有相同的值 - [??]未使用odr
我将5解释为特别";而不是在CCD_ 14的定义中使用的odr";;可以说,这一点在措辞上更为清晰。然而,如果使用了kFoo
odr(至少在foo
的定义中),我将其解释为由于违反了[basic.def.odr]/6,为违反odr和随后的未定义行为打开了大门。
Afaict[basic.def.odr]/3决定是否使用kFoo
,
变量x的名称显示为潜在求值表达式ex,除非将左值到右值的转换([conv.val])应用于x会产生一个不调用任何非平凡函数的常量表达式([expr.const]),并且如果x是对象,则ex是表达式e的潜在结果集的元素,其中,左值到右值的转换([conv.val])应用于e,或者e是一个丢弃的值表达式(Clause[expr])。[…]
但我很难理解kFoo
是否被视为使用的odr,例如,如果其地址在foo
的定义范围内,或者例如,如果它的地址在foo
的定义范围外,是否会影响是否满足[basic.def.odr]/6.2。
更多详细信息
特别是,考虑foo
是否定义为:
// #2
inline int foo(int arg) {
std::cout << "&kFoo in foo() = " << &kFoo << "n";
return arg * kFoo;
}
并且CCD_ 22和CCD_
int a() {
std::cout << "TU_a, &kFoo = " << &kFoo << "n";
return foo(1);
}
int b() {
std::cout << "TU_b, &kFoo = " << &kFoo << "n";
return foo(2);
}
则运行按顺序调用CCD_ 24和CCD_
TU_a, &kFoo = 0x401db8
&kFoo in foo() = 0x401db8 // <-- foo() in TU_a:
// &kFoo from TU_a
TU_b, &kFoo = 0x401dbc
&kFoo in foo() = 0x401db8 // <-- foo() in TU_b:
// !!! &kFoo from TU_a
即当从不同的a()
和b()
功能访问时TU本地kFoo
的地址,但当从foo()
访问时指向相同的kFoo
地址。
演示。
该程序(根据本节定义了foo
和a
/b
)是否具有未定义的行为?
一个现实生活中的例子是,这些constexpr变量表示数学常数,并且在内联函数的定义中,它们被用作实用数学函数(如std::max()
)的参数,CCD_34通过引用获取其参数。
在OP的std::max
示例中,确实发生了ODR冲突,并且程序是格式错误的NDR。为了避免这个问题,您可以考虑以下修复方法之一:
- 赋予
doMath
函数内部链接,或 - 在
doMath
中移动kTwo
的声明
表达式使用的变量被认为是odr使用的,除非有某种简单的证据表明对变量的引用可以被变量的编译时常数值取代,而不会改变表达式的结果。如果存在这样一个简单的证明,那么标准要求编译器执行这样的替换;因此,不使用该变量odr(特别是,它不需要定义,并且可以避免OP描述的问题,因为定义doMath
的翻译单元中没有实际引用kTwo
的定义)。然而,如果表达式太复杂,那么所有的赌注都会落空。编译器可能会仍然用它的值替换变量,在这种情况下,程序可能会如您所期望的那样工作;或者程序可能出现错误或崩溃。这就是IFNDR项目的现实。
变量直接通过引用绑定直接通过引用传递给函数的情况是一种常见的情况,其中变量的使用方式过于复杂,编译器不需要确定是否可以用其编译时常数值替换它。这是因为这样做必然需要检查函数的定义(例如本例中的std::max<int>
)。
你可以"帮助;通过写入CCD_ 42并将其用作CCD_;这防止了odr的使用,因为现在在调用函数之前会立即应用左值到右值的转换。我不认为这是一个好的解决方案(我推荐我之前提到的两个解决方案中的一个),但它有它的用途(GoogleTest使用它是为了避免在EXPECT_EQ(2, kTwo)
等语句中引入odr用途)。
如果你想了解更多关于如何理解odr使用的精确定义;表达式e的潜在结果&";,最好用一个单独的问题来解决。
doMath()定义如下:[…]的程序在两个不同的翻译单元中定义
doMath()
时是否具有未定义的行为(例如通过包含some_math.h
和随后使用doMath()
)?
是;这个特殊的问题在LWG2888和LWG2889中得到了强调,它们都通过P0607R0(标准库的内联变量)[emphasismine]为C++17解决了:
2888.库标记类型的变量需要是内联变量
[…]
库标记类型的变量需要是内联变量。否则,在多重翻译的内联函数中使用它们单位是ODR违规。
建议的更改:使piecewise_struct、allocater_arg、nullopt、,(在它们被制成常规标签之后的in_place_tag)、defer_,try_to_lock和approve_lock内联。
[…]
〔2017-03-12,后科纳〕解决由p0607r0。
2889.将constexpr全局变量标记为内联
C++标准库提供了许多constexpr全局变量。这些都会为无辜的用户代码带来ODR违规的风险这对新的ExecutionPolicy算法来说尤其糟糕,因为它们的常数总是通过引用传递的,所以任何使用内联函数中的算法会导致ODR冲突。
这可以通过将全局变量标记为内联来避免。
建议的更改:将内联说明符添加到:绑定占位符_1,_2,…,nullopt,piecewise_struct,allocater_arg,ignore,seq,par,中的par_unseq
[…]
〔2017-03-12,后科纳〕解决由p0607r0。
因此,在C++14中,在inline
变量之前,您自己的全局变量和库变量都存在这种风险。