【问题标题】:Is a constexpr array necessarily odr-used when subscripted?下标时是否必须使用 constexpr 数组?
【发布时间】:2026-02-08 17:05:02
【问题描述】:

给定以下代码:

struct A { static constexpr int a[3] = {1,2,3}; };

int main () {
  int a = A::a[0];
  int b  [A::a[1]];
}

A::a 一定是odr-usedint a = A::a[0] 中吗?


注意:这个问题代表了a debate in the Lounge的一个不那么火爆/不合逻辑/无穷无尽的版本。

【问题讨论】:

    标签: c++ c++11 language-lawyer constexpr


    【解决方案1】:

    第一次使用A::a

    int a = A::a[0];
    

    初始值设定项是一个常量表达式,但这并不能阻止A::a 在这里被odr-used。而且,确实,A::a 被这个表达式odr-used

    从表达式A::a[0]开始,我们来看看[basic.def.odr](3.2)/3(对于未来的读者,我使用的是N3936中的措辞): p>

    一个变量x [在我们的例子中,A::a],其名称显示为一个潜在的评估表达式 ex [在我们的例子中,id-expression A::a] 是 odr-used 除非

    • 将左值到右值的转换应用到x 会产生一个常量表达式 [确实如此] 不调用任何重要函数 [它不] 并且,

    • 如果x 是一个对象[它是],

      • ex 是表达式e潜在结果 集合中的一个元素,其中左值到右值的转换应用于e,或者e 是丢弃的-价值表达。

    那么:e 的可能值有哪些?表达式的潜在结果集是该表达式的一组子表达式(您可以通过阅读 [basic.def.odr](3.2)/2 来检查这一点),所以我们只需要考虑 ex 是子表达式的表达式。它们是:

    A::a
    A::a[0]
    

    其中,左值到右值的转换不会立即应用于A::a,因此我们只考虑A::a[0]。根据 [basic.def.odr](3.2)/2A::a[0] 的潜在结果集是空的,因此 A::aodr-used表达。

    现在,您可以争辩说我们首先将A::a[0] 重写为*(A::a + 0)。但这并没有改变:e 的可能值是

    A::a
    A::a + 0
    (A::a + 0)
    *(A::a + 0)
    

    其中,只有第四个应用了左值到右值的转换,并且再次,[basic.def.odr](3.2)/2 表示这组潜在结果*(A::a + 0) 为空。特别要注意,数组到指针的衰减不是左值到右值的转换 ([conv.lval](4.1)),即使它转换了数组左值到指针右值——这是数组到指针的转换([conv.array](4.2))。

    第二次使用A::a

    int b  [A::a[1]];
    

    根据标准,这与第一种情况没有什么不同。同样,A::a[1] 是一个常量表达式,因此这是一个有效的数组绑定,但仍然允许编译器在运行时发出代码来计算这个值,并且数组绑定仍然 odr-uses @ 987654350@.

    请特别注意,常量表达式(默认情况下)是可能求值的表达式。根据[basic.def.odr](3.2)/2

    一个表达式可能被计算,除非它是一个未计算的操作数(第 5 条)或其子表达式。

    [expr](5)/8 只是将我们重定向到其他子条款:

    在某些情况下,会出现未计算的操作数(5.2.8、5.3.3、5.3.7、7.1.6.2)。未计算的操作数不会被计算。

    这些子条款说(分别)某些typeid 表达式的操作数、sizeof 的操作数、noexcept 的操作数和decltype 的操作数是未计算的操作数。没有其他类型的未计算操作数。

    【讨论】:

    • 如果您还可以提供一些基本原理,那就太好了:为什么(似乎是)编译时常量需要定义? int b[A::a[1]];struct A { static constexpr auto a = 5; }; int b[A::a]; 有何不同?
    • @dyp 我希望区别在于规范的字母,因为它已在此处复制。而已。对我来说似乎是一个合乎逻辑的解释。
    • @dyp 我认为我们在讨论成为 [basic.def.odr]p2 的措辞时并没有真正考虑到这一点,但保持这些规则简单对我来说当然是有意义的。实现必须推迟某些操作以支持当前的 odr 使用规则;如果我们将数组索引添加到混合中,实现还必须推迟将A[I] 转换为*(A + I),因为它们的行为会有所不同。 (我们已经通过将rvalue[I] 设置为 xvalue 来涉足这些领域,所以现在也许是考虑这一变化的更好时机......)
    • @RichardSmith:就个人而言,我不明白为什么*(A + I) 也需要使用 ODR。 A 的运行时地址与表达式的结果无关。当然,标准是否支持它是另一个问题,但从概念上讲,我认为它不应该被 ODR 使用。
    • 你能澄清一件事吗?您说“表达式的一组潜在结果是该表达式的一组子表达式”,然后您声明 A::aA::a[0] 的子表达式(也是 A::a 的子表达式)。后来你说“A::a[0] 的潜在结果集是空的”,但你不是说A::aA::a[0] 的子表达式(因此是潜在结果)吗?为什么现在是空的?
    【解决方案2】:

    是的,A::a使用过

    在C++11中,相关的写法是3.2p2 [basic.def.odr]

    [...] 名称作为潜在求值表达式出现的变量是odr-used,除非它是一个满足出现在常量表达式(5.19)中的要求并且立即应用左值到右值转换 (4.1)。 [...]

    变量 A::a 的名称出现在声明 int a = A::a[0] 中,在完整表达式 A::a[0] 中,这是一个潜在的求值表达式。 A::a 是:

    • 一个对象
    • 满足出现在常量表达式中的要求

    但是,左值到右值的转换不会立即应用于A::a;它应用于表达式A::a[0]。实际上,左值到右值的转换可能不适用于数组类型 (4.1p1) 的对象。

    所以A::aodr-used


    自 C++11 起,规则有所拓宽。 DR712 是否“使用了条件表达式的整数常量操作数?”引入了表达式的潜在结果集的概念,这允许诸如x ? S::a : S::b 之类的表达式避免odr-use。然而,虽然潜在结果集尊重条件运算符和逗号运算符等运算符,但它不尊重索引或间接;所以A::a 在 C++14 的当前草案中仍然是odr-used(截至日期为 n3936)。

    [我相信这是 Richard Smith 的回答的浓缩版,但没有提到自 C++11 以来的变化。]

    When is a variable odr-used in C++14?,我们讨论了这个问题以及可能对第 3.2 节的措辞进行更改,以允许对数组进行索引或间接处理以避免odr-use

    【讨论】:

      【解决方案3】:

      不,它不是odr-used

      首先,您的数组及其元素都是 literal 类型:

      [C++11: 3.9/10]: 一个类型是文字类型,如果它是:

      • 标量类型;或
      • 类类型(第 9 条)与
      • 一个简单的复制构造函数,
      • 没有重要的移动构造函数,
      • 一个微不足道的析构函数,
      • 一个普通的默认构造函数或至少一个除复制或移动构造函数之外的 constexpr 构造函数,以及
      • 文字类型的所有非静态数据成员和基类;或
      • 文字类型的数组

      现在我们查找 odr-used 规则:

      [C++11: 3.2/2]: [..] 名称显示为潜在求值表达式的变量或非重载函数是odr-used,除非它是一个对象满足出现在常量表达式 (5.19) 中的要求,并立即应用左值到右值转换 (4.1)。 [..]

      这里我们提到了关于常量表达式的规则,其中不包含任何禁止你的初始化器成为常量表达式的内容;相关段落是:

      [C++11: 5.19/2]: 条件表达式是一个常量表达式,除非它涉及以下之一作为潜在的求值子表达式[..]::强>

      • [..]
      • 左值到右值的转换 (4.1),除非它应用于
        • 整数或枚举类型的左值,它引用具有先前初始化、使用常量表达式初始化的非易失性 const 对象,或
        • 字面量类型的左值,指的是用constexpr 定义的非易失性对象,或指此类对象的子对象,或
        • 字面量类型的左值,指的是用常量表达式初始化的非易失性临时对象;
      • [..]

      (不要被产生式的名字吓到,“conditional-expression”:它是constant-expression的唯一产生式,并且因此是我们正在寻找的那个。)

      然后,考虑A::a[0]*(A::a + 0) 的等价性,在数组到指针的转换之后,您有一个右值

      [C++11: 4.2/1]: 类型为“NT”或“T 未知边界数组”类型的左值或右值可以转换为“指向T 的指针”类型的右值。结果是指向数组第一个元素的指针。

      然后在这个右值上执行你的指针运算,结果也是一个右值,用于初始化a。这里没有任何左值到右值的转换,所以仍然没有违反“出现在常量表达式中的要求”。

      【讨论】:

      • @Yakk: ¶7: “订阅者对根据订阅者密码的任何使用或采取的行动负全部责任,并对通过订阅者帐户进行的所有活动承担全部责任,并同意并特此发布Network 和 Stack Exchange 不承担与此类活动有关的任何和所有责任。订阅者同意立即通知 Stack Exchange 任何实际或可疑的丢失、被盗或未经授权使用订阅者的帐户或密码。” ToS 中的任何内容均不禁止我将该授权授予我选择的任何人类、猫科动物或火神。 :-)
      • @Nikos:不能对来自同一个帐户的帖子投反对票,而且我的猫未满 13 岁,所以必须分享我的(根据 ToS 的¶1,上面链接)。 :-)
      • @MWid:a glvalue of literal type that refers to a non-volatile object defined with constexpr, or that refers to a sub-object of such an object 是否不能恰当地描述所说的转换?
      • @LightnessRacesinOrbit 我投了反对票,因为答案不正确。在 C++11 规则中,左值到右值的转换不会“立即应用于”变量,因此它是 odr-used。
      • 对不起,我的误会;在这种情况下,您的推理缺陷在于,正如理查德史密斯所说,左值到右值的转换没有“立即应用于”变量A::a
      最近更新 更多