首页 > 解决方案 > 在内联函数的定义中使用 constexpr 变量时可能的 ODR 违规(在 C++14 中)

问题描述

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

TLDR;问题

(...可能是 [basic.def.odr]/3;但是,如果这样的 constexpr 变量的地址是在内联函数定义的上下文中获取的,这是否可以在程序中静默引入 UB?)

doMath()TLDR 示例:执行如下定义的程序:

// 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 变量被隐式内联之前。

内联函数(具有外部链接)在与andfoo相关的两个翻译单元 (TU) 中使用,例如and ,因此应在这两个 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 如果满足,如果kFoofoo

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

我将 5 解释为特别“未在定义中foo使用 odr ”;可以说,这在措辞中可能更清楚。但是,如果kFoo odr-used(至少在 的定义中foo),我将其解释为由于违反 [basic.def.odr]/6 而对 odr-violations 和随后的未定义行为开放。

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

变量 x 其名称显示为潜在求值表达式 ex 被 ex odr 使用,除非将左值到右值转换 ([conv.lval]) 应用于 x 产生一个常量表达式 ([expr.const])不调用任何非平凡函数,如果 x 是一个对象,则 ex 是表达式 e 的潜在结果集合中的一个元素,其中左值到右值转换 ([conv.lval]) 应用于 e , 或 e 是丢弃值表达式(子句 [expr])。[...]

但我很难理解是否kFoo被认为是 odr-used 例如,如果它的地址在 的定义范围内foo,或者它的地址是否在定义之外,foo是否会影响 [basic.def. odr]/6.2 是否满足。


更多详细信息

特别是,考虑 iffoo定义为:

// #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

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

演示

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

一个真实的例子是这些 constexpr 变量表示数学常数的地方,以及在哪里使用它们,从内联函数的定义中,作为实用数学函数的参数,例如std::max(),它通过引用获取其参数。

标签: c++c++14language-lawyer

解决方案


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

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

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

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

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

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


推荐阅读