在内联函数的定义中使用 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;问题

(... 可能是 [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.cppb.cpp 关联的两个翻译单元 (TU) 中被 ODR 使用,比如 TU_aTU_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 如果满足 如果 kFoofoo:

  1. [OK] 是带有内部链接的 const
  2. [OK]用常量表达式初始化
  3. [OK] 在 foo
  4. 的所有定义中具有相同的文字类型
  5. [OK] 在 foo
  6. 的所有定义中具有相同的值
  7. [??] 未使用 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.

此程序(fooa/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 变量出现之前,您自己的全局变量和库变量都存在这种风险。