在内联函数的定义中使用 constexpr 变量时可能发生 ODR 违规(在 C++14 中)

dfr*_*fri 7 c++ language-lawyer c++14

(注意!这个问题特别涵盖了 C++17 中引入内联变量之前 C++14 的状态)

太长了;问题

  • 什么构成了在内联函数定义中使用 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&)
Run Code Online (Sandbox Code Playgroud)

一旦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
Run Code Online (Sandbox Code Playgroud)

为 C++14 编译,特别是在内联变量之前,因此在 constexpr 变量隐式内联之前。

内联函数(具有外部链接)在与和foo关联的两个翻译单元 (TU) 中使用 odr,例如和,因此应在这两个 TU 中定义 ( [basic.def.odr]/4 )。a.cppb.cppTU_aTU_b

[basic.def.odr]/6涵盖了何时可能出现此类多个定义(不同的 TU)的要求,特别是 /6.1 和 /6.2 与此上下文相关[强调我的]:

程序中可以有多个具有外部链接的内联函数定义,前提是每个定义出现在不同的翻译单元中,并且定义满足以下要求。给定这样一个名为 D 的实体,在多个翻译单元中定义,则

  • /6.1 D的每个定义应由相同的标记序列组成;和

  • /6.2 在 D 的每个定义中,根据 [basic.lookup] 查找的相应名称应引用 D 定义内定义的实体,或者在重载解析后应引用相同的实体([over.match] ) 和部分模板特化 ([temp.over]) 匹配之后,除了如果对象在 D 的所有定义中具有相同的文字类型,则名称可以引用具有内部链接或无链接的非易失性 const 对象,并且对象用常量表达式([expr.const])初始化,并且该对象不是 odr-used,并且该对象在 D 的所有定义中具有相同的值;和

  • ...

如果 D 的定义不满足这些要求,则行为未定义。

/6.1已实现。

/6.2 如果满足kFoo如果foo:

  1. [OK] 是具有内部链接的常量
  2. [OK] 用常量表达式初始化
  3. [OK] 在所有定义中具有相同的文字类型foo
  4. [OK] 在所有定义中具有相同的值foo
  5. [??] 未使用 odr。

我将 5 特别解释为“在定义中foo未使用 odr ”;这一点在措辞上可能会更清楚。但是,如果kFoo odr使用(至少在 的定义中foo),我将其解释为由于违反 [basic.def.odr]/6 而导致 odr 违规和随后的未定义行为。

Afaict [basic.def.odr]/3控制是否kFoo使用 odr,

名称显示为潜在求值表达式 ex 的变量 x 由 ex odr 使用,除非将左值到右值转换 ([conv.lval]) 应用于 x 生成常量表达式 ([expr.const])不调用任何非平凡函数,并且如果 x 是对象,则 ex 是表达式 e 的潜在结果集合的元素,其中左值到右值转换 ([conv.lval]) 应用于 e ,或 e 是丢弃值表达式(子句 [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; 
}
Run Code Online (Sandbox Code Playgroud)

和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); 
}
Run Code Online (Sandbox Code Playgroud)

然后运行一个程序,该程序调用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
Run Code Online (Sandbox Code Playgroud)

kFoo即从不同的a()和函数访问时, TU-local 的地址,但从访问时b()指向相同的地址。kFoofoo()

演示。

该程序(根据本节定义foo和a/定义)是否具有未定义的行为?b

现实生活中的示例是,这些 constexpr 变量表示数学常量,以及在内联函数的定义中使用它们作为实用数学函数的参数,例如 ,std::max()它通过引用获取其参数。

Bri*_*ian 5

在 OP 的示例中std::max,确实发生了 ODR 违规,并且该程序是格式不正确的 NDR。为了避免此问题,您可以考虑以下修复之一:

  • 给出doMath函数内部链接,或者
  • kTwo移动里面的声明doMath

表达式使用的变量被认为是 odr-used,除非有某种简单的证明可以证明对变量的引用可以被变量的编译时常量值替换而不改变表达式的结果。如果存在这样一个简单的证明,那么标准要求编译器执行这样的替换;因此,该变量不是 odr 使用的(特别是,它不需要定义,并且可以避免 OP 描述的问题,因为定义的翻译单元doMath实际上不会引用 的定义kTwo)。然而,如果表达式太复杂,那么一切就都失败了。编译器仍可能用变量的值替换该变量,在这种情况下,程序可能会按您的预期运行;否则程序可能会出现错误或崩溃。这就是 IFNDR 计划的现实。

变量立即通过引用传递给函数(直接引用绑定)的情况是一种常见情况,其中变量的使用方式过于复杂,并且编译器不需要确定它是否可以替换为其编译时常量值。这是因为这样做必然需要检查函数的定义(例如std::max<int>本例中)。

您可以通过编写并使用它作为而不是其自身的int(kTwo)参数来“帮助”编译器;这可以防止 ODR 使用,因为现在在调用函数之前立即应用左值到右值的转换。我不认为这是一个很好的解决方案(我推荐我之前提到的两个解决方案之一),但它有它的用途(GoogleTest 使用它是为了避免在类似 的语句中引入 odr-uses )。std::maxkTwoEXPECT_EQ(2, kTwo)

如果您想更多地了解如何理解 odr-use 的精确定义,涉及“表达式e ... 的潜在结果”,最好用一个单独的问题来解决。