为什么枚举值的initializer_list不被视为常量表达式?

时间:2019-01-17 06:32:39

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

在以下代码中(在本地和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-id是非常数。

其次,MSVC抱怨读取的生存期过长,但是group1的生存期及其值应在print中的整个使用中扩展。

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

最后,如果删除constexpr,除gcc以外的所有对象都会很高兴地编译并运行此代码,而不会出现任何问题。授予它在这种情况下不是不必要,但我看不出有什么充分的理由使其不起作用。

同时,只有当参数类型更改为std::initializer_list<const Types>时,gcc才会编译并运行代码-并且进行此更改会导致它无法在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;它必须以通用的容器/迭代器概念为模板,这很不方便。

2 个答案:

答案 0 :(得分:9)

初始化std::initializer_list时,会发生以下情况:

  

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

     

5构造了一个std :: initializer_­list类型的对象   从初始化程序列表中,就好像实现已生成并   实现了“ N const E的数组”类型的prvalue ,其中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核心常量   表示一个实体的表达式,该实体是   常量表达式(如下定义)或prvalue核心常量   其值满足以下约束的表达式:

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

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

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

最终,这一切都有些模糊。

答案 1 :(得分:5)

看来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中指定的初始化程序列表。复制初始化程序列表不会复制基础元素。 —尾注]
  2.   

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