在内联函数的定义中使用 constexpr 变量时可能的 ODR 违规(在 C++14 中)
Possible ODR-violations when using a constexpr variable in the definition of an inline function (in C++14)
(注意!这个问题特别涉及 C++14 的状态,在 C++17 中引入内联变量之前)
TLDR;问题
- 什么构成了在内联函数定义中使用 constexpr 变量的 odr 用法,以至于该函数的多个定义违反了 [basic.def.odr]/6?
(... 可能是 [basic.def.odr]/3; 但是一旦这样一个 constexpr 变量的地址在内联函数的定义?)
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) 中被 ODR 使用,比如 TU_a
和 TU_b
,因此应在这两个 TU ([basic.def.odr]/4).
中定义
[basic.def.odr]/6 涵盖了何时可能出现此类多个定义(不同 TU)的要求,特别是 /6.1 和 /6.2 在这种情况下是相关的 [emphasis 我的]:
There can be more than one definition of a [...] inline function with external linkage [...] in a program
provided that each definition appears in a different translation unit,
and provided the definitions satisfy the following requirements. Given
such an entity named D defined in more than one translation unit, then
/6.1 each definition of D shall consist of the same sequence of tokens; and
/6.2 in each definition of D, corresponding names, looked up according to [basic.lookup], shall refer to an entity defined within
the definition of D, or shall refer to the same entity, after overload
resolution ([over.match]) and after matching of partial template
specialization ([temp.over]), except that a name can refer to a
non-volatile const object with internal or no linkage if the object
has the same literal type in all definitions of D, and the object is
initialized with a constant expression ([expr.const]), and the object
is not odr-used, and the object has the same value in all definitions
of D; and
...
If the definitions of D do not satisfy these requirements, then the behavior is undefined.
/6.1满足。
/6.2 如果满足 如果 kFoo
在 foo
:
- [OK] 是带有内部链接的 const
- [OK]用常量表达式初始化
- [OK] 在
foo
的所有定义中具有相同的文字类型
- [OK] 在
foo
的所有定义中具有相同的值
- [??] 未使用 ODR。
我将 5 解释为特别“未在 foo
的定义中使用 ”;这可以说在措辞中更清楚。但是,如果 kFoo
是 odr-used(至少在 foo
的定义中),我将其解释为开放 odr-violations 和随后的未定义行为,由于违反 [basic.def.odr]/6.
Afaict [basic.def.odr]/3 控制 kFoo
是否使用 odr,
A variable x whose name appears as a potentially-evaluated expression ex is odr-used by ex unless applying the lvalue-to-rvalue conversion ([conv.lval]) to x yields a constant expression ([expr.const]) that does not invoke any non-trivial functions and, if x is an object, ex is an element of the set of potential results of an expression e, where either the lvalue-to-rvalue conversion ([conv.lval]) is applied to e, or e is a discarded-value expression (Clause [expr]). [...]
但我很难理解 kFoo
是否被视为 odr-used,例如如果它的地址在 foo
的定义范围内,或者例如它的地址是否在 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 // <-- 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-local kFoo
的地址,但从访问时指向相同的 kFoo
地址foo()
.
DEMO.
此程序(foo
和 a
/b
根据本节定义)是否有未定义的行为?
一个现实生活中的例子是,这些 constexpr 变量代表数学常数,以及它们在哪里使用,从内联函数的定义中,作为实用数学函数的参数,例如 std::max()
,它需要它的论点通过引用。
在 std::max
的 OP 示例中,确实发生了 ODR 违规,并且该程序是格式错误的 NDR。为避免此问题,您可以考虑以下修复之一:
- 给
doMath
函数内部链接,或者
- 将
kTwo
的声明移到 doMath
中
表达式使用的变量被认为是 odr-used 除非有某种简单的证据表明对变量的引用可以被变量的编译时常量值替换而不改变表达式的结果。如果存在这样一个简单的证明,那么标准要求编译器执行这样的替换;因此该变量未使用 odr(特别是,它不需要定义,并且可以避免 OP 描述的问题,因为 none 中的翻译单元doMath
的定义实际上会引用 kTwo
的定义)。但是,如果表达式太复杂,那么所有的赌注都会落空。编译器可能仍然用它的值替换变量,在这种情况下,程序可能会按您预期的那样工作;否则程序可能会出现错误或崩溃。这就是 IFNDR 计划的现实。
变量通过引用直接传递给函数的情况,直接引用绑定,是一种常见的情况,变量的使用方式过于复杂,编译器不需要判断是否或不可以用它的编译时常量值代替。这是因为这样做必然需要检查函数的定义(例如本例中的 std::max<int>
)。
您可以通过编写 int(kTwo)
并将其用作 std::max
的参数而不是 kTwo
本身来“帮助”编译器;这防止了 odr 的使用,因为左值到右值的转换现在在调用函数之前立即应用。我不认为这是一个很好的解决方案(我推荐我之前提到的两个解决方案之一)但是它有它的用途(GoogleTest 使用它是为了避免在像 EXPECT_EQ(2, kTwo)
这样的语句中引入 odr-uses)。
如果您想了解更多关于如何理解odr-use的精确定义,涉及“表达式e...的潜在结果”,那最好用一个单独的问题解决。
Does a program where doMath() defined as follows: [...] have undefined behaviour as soon as doMath()
is defined in two different translation units (say by inclusion of some_math.h
and subsequent use of doMath()
)?
是; LWG2888 and LWG2889 which were both resolved for C++17 by P0607R0(标准库的内联变量)[强调我的]:
2888. Variables of library tag types need to be inline variables
[...]
The variables of library tag types need to be inline variables.
Otherwise, using them in inline functions in multiple translation
units is an ODR violation.
Proposed change: Make piecewise_construct, allocator_arg, nullopt,
(the in_place_tags after they are made regular tags), defer_lock,
try_to_lock and adopt_lock inline.
[...]
[2017-03-12, post-Kona] Resolved by p0607r0.
2889. Mark constexpr global variables as inline
The C++ standard library provides many constexpr global variables.
These all create the risk of ODR violations for innocent user code.
This is especially bad for the new ExecutionPolicy algorithms, since
their constants are always passed by reference, so any use of those
algorithms from an inline function results in an ODR violation.
This can be avoided by marking the globals as inline.
Proposed change: Add inline specifier to: bind placeholders _1, _2,
..., nullopt, piecewise_construct, allocator_arg, ignore, seq, par,
par_unseq in
[...]
[2017-03-12, post-Kona] Resolved by p0607r0.
因此,在 C++14 中,在 inline
变量出现之前,您自己的全局变量和库变量都存在这种风险。
(注意!这个问题特别涉及 C++14 的状态,在 C++17 中引入内联变量之前)
TLDR;问题
- 什么构成了在内联函数定义中使用 constexpr 变量的 odr 用法,以至于该函数的多个定义违反了 [basic.def.odr]/6?
(... 可能是 [basic.def.odr]/3; 但是一旦这样一个 constexpr 变量的地址在内联函数的定义?)
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) 中被 ODR 使用,比如 TU_a
和 TU_b
,因此应在这两个 TU ([basic.def.odr]/4).
[basic.def.odr]/6 涵盖了何时可能出现此类多个定义(不同 TU)的要求,特别是 /6.1 和 /6.2 在这种情况下是相关的 [emphasis 我的]:
There can be more than one definition of a [...] inline function with external linkage [...] in a program provided that each definition appears in a different translation unit, and provided the definitions satisfy the following requirements. Given such an entity named D defined in more than one translation unit, then
/6.1 each definition of D shall consist of the same sequence of tokens; and
/6.2 in each definition of D, corresponding names, looked up according to [basic.lookup], shall refer to an entity defined within the definition of D, or shall refer to the same entity, after overload resolution ([over.match]) and after matching of partial template specialization ([temp.over]), except that a name can refer to a non-volatile const object with internal or no linkage if the object has the same literal type in all definitions of D, and the object is initialized with a constant expression ([expr.const]), and the object is not odr-used, and the object has the same value in all definitions of D; and
...
If the definitions of D do not satisfy these requirements, then the behavior is undefined.
/6.1满足。
/6.2 如果满足 如果 kFoo
在 foo
:
- [OK] 是带有内部链接的 const
- [OK]用常量表达式初始化
- [OK] 在
foo
的所有定义中具有相同的文字类型
- [OK] 在
foo
的所有定义中具有相同的值
- [??] 未使用 ODR。
我将 5 解释为特别“未在 foo
的定义中使用 ”;这可以说在措辞中更清楚。但是,如果 kFoo
是 odr-used(至少在 foo
的定义中),我将其解释为开放 odr-violations 和随后的未定义行为,由于违反 [basic.def.odr]/6.
Afaict [basic.def.odr]/3 控制 kFoo
是否使用 odr,
A variable x whose name appears as a potentially-evaluated expression ex is odr-used by ex unless applying the lvalue-to-rvalue conversion ([conv.lval]) to x yields a constant expression ([expr.const]) that does not invoke any non-trivial functions and, if x is an object, ex is an element of the set of potential results of an expression e, where either the lvalue-to-rvalue conversion ([conv.lval]) is applied to e, or e is a discarded-value expression (Clause [expr]). [...]
但我很难理解 kFoo
是否被视为 odr-used,例如如果它的地址在 foo
的定义范围内,或者例如它的地址是否在 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 // <-- 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-local kFoo
的地址,但从访问时指向相同的 kFoo
地址foo()
.
DEMO.
此程序(foo
和 a
/b
根据本节定义)是否有未定义的行为?
一个现实生活中的例子是,这些 constexpr 变量代表数学常数,以及它们在哪里使用,从内联函数的定义中,作为实用数学函数的参数,例如 std::max()
,它需要它的论点通过引用。
在 std::max
的 OP 示例中,确实发生了 ODR 违规,并且该程序是格式错误的 NDR。为避免此问题,您可以考虑以下修复之一:
- 给
doMath
函数内部链接,或者 - 将
kTwo
的声明移到doMath
中
表达式使用的变量被认为是 odr-used 除非有某种简单的证据表明对变量的引用可以被变量的编译时常量值替换而不改变表达式的结果。如果存在这样一个简单的证明,那么标准要求编译器执行这样的替换;因此该变量未使用 odr(特别是,它不需要定义,并且可以避免 OP 描述的问题,因为 none 中的翻译单元doMath
的定义实际上会引用 kTwo
的定义)。但是,如果表达式太复杂,那么所有的赌注都会落空。编译器可能仍然用它的值替换变量,在这种情况下,程序可能会按您预期的那样工作;否则程序可能会出现错误或崩溃。这就是 IFNDR 计划的现实。
变量通过引用直接传递给函数的情况,直接引用绑定,是一种常见的情况,变量的使用方式过于复杂,编译器不需要判断是否或不可以用它的编译时常量值代替。这是因为这样做必然需要检查函数的定义(例如本例中的 std::max<int>
)。
您可以通过编写 int(kTwo)
并将其用作 std::max
的参数而不是 kTwo
本身来“帮助”编译器;这防止了 odr 的使用,因为左值到右值的转换现在在调用函数之前立即应用。我不认为这是一个很好的解决方案(我推荐我之前提到的两个解决方案之一)但是它有它的用途(GoogleTest 使用它是为了避免在像 EXPECT_EQ(2, kTwo)
这样的语句中引入 odr-uses)。
如果您想了解更多关于如何理解odr-use的精确定义,涉及“表达式e...的潜在结果”,那最好用一个单独的问题解决。
Does a program where doMath() defined as follows: [...] have undefined behaviour as soon as
doMath()
is defined in two different translation units (say by inclusion ofsome_math.h
and subsequent use ofdoMath()
)?
是; LWG2888 and LWG2889 which were both resolved for C++17 by P0607R0(标准库的内联变量)[强调我的]:
2888. Variables of library tag types need to be inline variables
[...]
The variables of library tag types need to be inline variables. Otherwise, using them in inline functions in multiple translation units is an ODR violation.
Proposed change: Make piecewise_construct, allocator_arg, nullopt, (the in_place_tags after they are made regular tags), defer_lock, try_to_lock and adopt_lock inline.
[...]
[2017-03-12, post-Kona] Resolved by p0607r0.
2889. Mark constexpr global variables as inline
The C++ standard library provides many constexpr global variables. These all create the risk of ODR violations for innocent user code. This is especially bad for the new ExecutionPolicy algorithms, since their constants are always passed by reference, so any use of those algorithms from an inline function results in an ODR violation.
This can be avoided by marking the globals as inline.
Proposed change: Add inline specifier to: bind placeholders _1, _2, ..., nullopt, piecewise_construct, allocator_arg, ignore, seq, par, par_unseq in
[...]
[2017-03-12, post-Kona] Resolved by p0607r0.
因此,在 C++14 中,在 inline
变量出现之前,您自己的全局变量和库变量都存在这种风险。