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

Why is an initializer_list of enum values not considered a constant expression?

本文关键字:常量 表达式 list 枚举 initializer 为什么      更新时间:2023-10-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>

为什么?

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

其次,MSVC抱怨读取超出了生存期,但group1的生存期及其值应该在print中的整个使用过程中延长。

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

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

同时,只有当参数类型更改为std::initializer_list<const Types>时,gcc才会编译并运行代码,而进行此更改会导致它在MSVC和clang中都无法编译。

(有趣的是:更改了参数类型后,gcc8确实成功编译并运行了包括constexpr在内的代码,其中gcc9出错。)


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;它必须以通用容器/迭代器概念为模板,这很不方便。

初始化std::initializer_list时,情况如下:

[dcl.init.list](emphasis mine)

5 std类型的对象​::​initializer_list已构造从初始值设定项列表,就好像实现生成了和具体化了"array of 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对象。由于std::initializer_list通过[dcl.init.list]/6对该临时对象具有寿命扩展的影响,因此当您将group1声明为静态对象时,clang和gcc似乎也将数组分配为静态对象,这使得初始化的良好形式化仅取决于std::initializer_list是否为文字类型,并且所使用的构造函数是否为constexpr

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

似乎std::initializer_list(在C++17中)还没有满足文字类型的要求(这是constexpr变量类型必须满足的要求)。

关于它在C++14中是否这样做的讨论可以在这篇文章中找到:为什么std::initializer_list没有被定义为文字类型?这本身就是讨论声明constexpr initializer_list对象合法吗?

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

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

[基本类型]/10.5

(10.5)一个可能的cv限定类类型(第12条),它具有以下属性:
(10.5.1)-它有一个平凡的析构函数,
,或至少有一个constexpr构造函数或构造函数模板(可能从基类继承(10.3.3))move构造函数,
(10.5.3)-如果它是并集,则至少有一个非静态数据成员是非易失性文字类型,
[10.5.4]-如果它不是并集,那么它的所有非静态数据元素和基类属于非易失性文字类型

[iinitializer_list.syn]/1

  1. initializer_list类型的对象提供对const E类型的对象数组的访问。[注意:一对指针或一个指针加上一个长度将是initializer_list的明显表示。initializer_列表用于实现11.6.4中指定的初始值设定项列表。复制初始值设定项列表不会复制基础元素--尾注]

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