在以下代码中(在本地和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
;它必须以通用的容器/迭代器概念为模板,这很不方便。
答案 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]/6对std::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)最终工作草案的引文:
(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的对象提供对const E类型的对象数组的访问。[注意:一对指针或一个指针加上一个长度显然是initializer_list的表示形式。 initializer_list用于实现11.6.4中指定的初始化程序列表。复制初始化程序列表不会复制基础元素。 —尾注]
这就是为什么声明constexpr initializer_list
对象不合法的原因。