【问题标题】:Why is an initializer_list of enum values not considered a constant expression?为什么枚举值的 initializer_list 不被视为常量表达式?
【发布时间】:2019-06-11 07:36:16
【问题描述】:

在以下代码中(在本地和 Wandbox 上测试):

#include <iostream>

enum Types
{
    A, B, C, D
};

void print(std::initializer_list<Types> types)
{
    for (auto type : types)
    {
        std::cout << type << std::endl;
    }
}

int main()
{
    constexpr auto const group1 = { A, D };
    print(group1);
    return 0;
}

MSVC 15.8.5 编译失败:

error C2131: expression did not evaluate to a constant
note: failure was caused by a read of a variable outside its lifetime
note: see usage of '$S1'

(均指包含constexpr的行)

Clang 8 (HEAD) 报告:

error: constexpr variable 'group1' must be initialized by a constant expression
    constexpr auto const group1 = { A, D };
                         ^        ~~~~~~~~
note: pointer to subobject of temporary is not a constant expression
note: temporary created here
    constexpr auto const group1 = { A, D };
                                  ^

gcc 9 (HEAD) 报告:

In function 'int main()':
error: 'const std::initializer_list<const Types>{((const Types*)(&<anonymous>)), 2}' is not a constant expression
   18 |     constexpr auto const group1 = { A, D };
      |                                          ^
error: could not convert 'group1' from 'initializer_list<const Types>' to 'initializer_list<Types>'
   19 |     print(group1);
      |           ^~~~~~
      |           |
      |           initializer_list<const Types>

为什么?

首先,他们显然都认为 enum-ids 是非常量的,尽管它们显然实际上是众所周知的编译时常量值。

其次,MSVC 抱怨在生命周期之外读取,但 group1 的生命周期及其值应该在 print 的整个使用过程中延伸。

第三,gcc 有一个奇怪的 const-vs-non-const 抱怨,我无法理解,因为初始化列表总是 const。

最后,如果 constexpr 被删除,除了 gcc 之外的所有代码都将愉快地编译和运行这段代码而不会出现任何问题。当然,在这种情况下它不是必要的,但我看不出有什么好的理由让它不起作用。

同时 gcc 只会在参数类型更改为 std::initializer_list&lt;const Types&gt; 时编译和运行代码——并且进行此更改会导致它在 MSVC 和 clang 中都无法编译。

(有趣的是:gcc 8,随着参数类型的变化,确实成功编译并运行了包括constexpr在内的代码,其中gcc 9出错了。)


FWIW,将声明更改为:

    constexpr auto const group1 = std::array<Types, 2>{ A, D };

在所有三个编译器上编译和运行。因此,行为不端的可能是 initializer_list 本身,而不是枚举值。但是语法更烦人。 (使用合适的make_array 实现会稍微不那么烦人,但我仍然不明白为什么原始版本无效。)


    constexpr auto const group1 = std::array{ A, D };

也可以使用,这要归功于 C++17 模板归纳。虽然现在print 不能接受initializer_list;它必须在通用容器/迭代器概念上进行模板化,这很不方便。

【问题讨论】:

  • 我喜欢这个问题,我想知道它是否是一个错误(可以登录到编译器的跟踪器上吗?)。只是出于好奇,它在 Enum 类中的表现如何?
  • 枚举类的行为完全一样,除了在print 中,您需要显式转换为int,因为隐式转换被禁用。
  • 即使没有枚举(只是简单的 int),我在 VS 2019 中得到“表达式没有计算为常量”,而 GCC 10 和 clang 10 编译如下:#include &lt;initializer_list&gt; constexpr static std::initializer_list&lt;int&gt; a = {1, 2};

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


【解决方案1】:

当您初始化 std::initializer_list 时,会发生如下情况:

[dcl.init.list](强调我的)

5 构造了一个 std​::​initializer_list 类型的对象 从初始化列表好像实现生成和 实现类型为“N const E 数组” 的纯右值,其中 N 是 初始值设定项列表中的元素数。该数组的每个元素 使用初始化器的相应元素进行复制初始化 列表,std​::​initializer_list 对象被构造为 参考那个数组。 [ 注意:构造函数或转换函数 选择的副本应在上下文中可访问 初始化列表。 — 尾注 ] 如果需要缩小转换 要初始化任何元素,程序格式错误。 [ 示例

struct X {
  X(std::initializer_list<double> v);
};
X x{ 1,2,3 };

初始化的实现方式大致相当于 这个:

const double __a[3] = {double{1}, double{2}, double{3}};
X x(std::initializer_list<double>(__a, __a+3));

假设实现可以构造一个initializer_list 带有一对指针的对象。 — 结束示例 ]

该临时数组如何用于初始化 std::initializer_list 决定了 initializer_list 是否使用常量表达式进行初始化。最终,根据示例(尽管是非规范性的),初始化将采用数组的地址或其第一个元素,这将产生一个指针类型的值。这不是一个有效的常量表达式。

[expr.const](强调我的)

5一个常量表达式要么是一个glvalue核心常量 引用作为 a 的允许结果的实体的表达式 常量表达式(定义如下),或纯右值核心常量 其值满足以下约束的表达式:

  • 如果值是类类型的对象,则每个引用类型的非静态数据成员引用一个实体,该实体是一个允许的结果 常量表达式,
  • 如果该值是指针类型,它包含一个具有静态存储持续时间的对象的地址,该地址超过了这样的结尾 一个对象([expr.add]),一个函数的地址,或者一个空指针 价值,以及
  • 如果该值是类或数组类型的对象,则每个子对象都满足该值的这些约束。

一个实体是一个常量表达式的允许结果,如果它是一个 具有不是临时的静态存储持续时间的对象 对象或者是一个临时对象,其值满足上述条件 约束,或者它是一个函数。

如果数组是静态对象,那么该初始化程序将构成一个有效的常量表达式,可用于初始化constexpr 对象。由于std::initializer_list[dcl.init.list]/6 的临时对象具有生命周期延长的效果,所以当您declare group1 as a static object 时,clang 和 gcc 似乎也将数组分配为静态对象,这使得初始化的格式正确仅取决于是否std::initializer_list 是文字类型,使用的构造函数是 constexpr

最终,一切都有些模糊。

【讨论】:

    【解决方案2】:

    看来std::initializer_list 尚未(在C++17 中)满足literal type 的要求(这是constexpr variable 类型必须满足的要求)。

    在这篇文章中找到了关于它是否在 C++14 中这样做的讨论:Why isn't std::initializer_list defined as a literal type? 这本身就是讨论Is it legal to declare a constexpr initializer_list object?的帖子的后续内容@

    我将 C++14 相关文章(C++14 标准)中提供的引用与最终工作草案(C++17 标准)进行了比较,它们是相同的。 所以没有明确要求std::initializer_list 应该是文字类型。

    来自 C++17 (n4659) 最终工作草案的引用:

    [basic.types]/10.5

    (10.5) 一个可能有 cv 限定的类类型(第 12 条),它具有所有 以下属性:
    (10.5.1) — 它有一个简单的析构函数,
    (10.5.2) — 它要么是闭包类型 (8.1.5.1),要么是聚合类型 (11.6.1), 或至少有一个 constexpr 构造函数或构造函数模板 (可能从基类继承(10.3.3))不是副本或 移动构造函数,
    (10.5.3) — 如果它是一个联合,则它的至少一个非静态数据成员是非易失性文字类型,并且
    (10.5.4) — 如果它不是联合,则它的所有非静态数据成员和基 类是非易失性文字类型

    [initializer_list.syn]/1

    1. initializer_list 类型的对象提供对 const E 类型对象数组的访问。[注意:一对指针或一个指针加上一个长度显然是 initializer_list 的表示。 initializer_list 用于实现 11.6.4 中指定的初始化列表。复制初始化列表不会复制底层元素。 ——尾注]

    这就是为什么声明 constexpr initializer_list 对象是不合法的原因。

    【讨论】:

    • 那么std::array,它接受initializer_list 构造函数参数,是否满足constexpr?为什么编译器不能类似地构造一个支持数组和一个initializer_list作为constexpr
    • 我想从技术上讲这是聚合初始化,而不是实际使用 initializer_list 构造函数。但这仍然很奇怪,这被区别对待了。
    • @Miral std::array 不接受std::initializer_list
    • 我确实说过。但是聚合初始化和initializer_list 构造函数应该是等价的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-05-02
    • 1970-01-01
    • 1970-01-01
    • 2018-07-01
    • 2018-06-01
    • 2011-10-22
    • 1970-01-01
    相关资源
    最近更新 更多