为什么基于范围的for循环遍历大括号初始列表非法C ++

时间:2019-11-07 07:47:44

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

这是针对以下问题的后续问题:

Innocent range based for loop not working

在此问题中,使用的语法是:

    int a{},b{},c{},d{};

    for (auto& i : {a, b, c, d}) {
        i = 1;
    }

那不是合法的c ++。 (为使其工作,必须发明一种新的容器类型,该类型在内部存储指针或引用)

这仅仅是两个不同概念的副作用吗?还是为什么不允许这样做?

乍一看,这似乎是推动语言发展的一个念头,但是在这些事情上我知道有很多工作要做。我想这是有问题的,或者可能只是考虑不多。

我在猜测答案可能是这样的:

考虑到initializer_list / arrays / brace-init列表/ etc的方式,这在某种程度上是有问题的。是经过设计的(并且“修复”对于编译器编写者来说太难了,或者在一般情况下执行效果不佳)。

这将需要一个特殊的规则,或者可能需要进行一些基本语言更改(例如更改为initializer_list)。

阅读起来会模棱两可/不清楚。

3 个答案:

答案 0 :(得分:4)

我认为关于此无效的最重要部分是cppreference中给出的原因(自C ++ 14起,重点是我的):

  

基础数组是类型为const T[N] 的临时数组,其中   每个元素均已复制初始化(缩小转换范围除外)   是无效的)   初始化列表。基础数组的生存期与   任何其他临时对象,除了初始化   数组中的initializer_list对象延长了   数组完全类似于将引用绑定到临时(具有相同的   异常,例如用于初始化非静态类成员的异常)。的   基础数组可以分配在只读存储器中

因此在for循环中引用非常量是无效的。另外,如注释中所述,引用数组不是有效的C ++构造

相关cpp标准为:http://eel.is/c++draft/support.initlist.access

答案 1 :(得分:4)

  

这将需要一个特殊的规则,或者可能需要进行一些基本语言更改(例如更改为select * from booking as b join booking_detail as bd on b.bookingId = bd.bookingId WHERE b.bookingId = 9 )。

改变确实需要是根本的。问题不在于如何实现initializer_list

根据[dcl.ref]

  

没有引用的引用,没有引用的数组,也没有指向引用的指针。

这很有意义,因为严格来说引用不是对象。引用不需要存储。

由于引用数组不合法,因此有一些解决方法:

使用指针数组

initializer_list

使用for (auto* i : {&a, &b, &c, &d}) { *i = 1; } 调味。请注意,我已经使用std::reference_wrapper而不是int&来不必使用auto&

i.get() = 1
for (int& i : {std::ref(a), std::ref(b), std::ref(c), std::ref(d)}) {
    i = 1;
}

如果经常使用,请帮忙:

for (int& i : std::initializer_list<std::reference_wrapper<int>>{a,b,c,d}) {
    i = 1;
}

答案 2 :(得分:2)

它不起作用,因为它不起作用。这就是功能的设计。该功能为list initialization,顾名思义就是要初始化某些东西。

当C ++ 11引入initializer_list时,这样做的目的恰恰是一个目的:允许系统从braced-init-list生成值数组并将它们传递给正确标记的构造函数(可能是间接的),以便对象可以使用该值序列对其进行初始化。在这种情况下,“适当的标记”是构造函数将新创建的std::initializer_list类型作为其第一个/唯一参数。这就是initializer_list作为类型的目的。

概括地说,

初始化不应该修改给定的值。支持该列表的数组是临时数组,这一事实也使输入值在逻辑上不可修改的想法倍增。如果您有:

std::vector<int> v = {1, 2, 3, 4, 5};

我们要赋予编译器自由,以使5个元素的数组成为已编译二进制文件中的静态数组,而不是使该函数的堆栈大小膨胀的堆栈数组。更重要的是,我们在逻辑上希望将braced-init-list视为一个文字。

而且我们不允许修改文字。

您试图使{a, b, c, d}成为一系列可修改的引用,实际上是在试图将已经存在的构造用于一个目的并将其转变为用于另一个目的的构造。您没有初始化任何东西;您只是在使用看似方便的语法,而该语法恰巧会生成可迭代的值列表。但是该语法称为“括号- init -list”,它会生成一个 initializer 列表。该术语不是偶然的。

如果您使用针对X的工具并尝试劫持Y,那么您很可能会在以后的某个地方遇到粗糙的边缘。所以它不起作用的原因是这不是意图;这些不是括号初始列表和initializer_list的目的。

接下来,您可能会说:“如果是这种情况,那么如果braced-init-lists仅用于初始化,那么for(auto i: {1, 2, 3, 4, 5})到底能工作吗?”

从前,它不是;在C ++ 11中,这是不正确的。只有在C ++ 14中,auto l = {1, 2, 3, 4};才成为合法语法。一个auto变量被允许自动推导一个括号初始列表为initializer_list

基于范围的for对范围类型使用auto推导,因此它继承了此功能。这自然使人们相信,括号初始列表是建立值的范围,而不是初始化事物。

简而言之,某人的便利功能使您相信旨在初始化对象的构造只是创建数组的一种快速方法。从来没有。


已经确定支撑不是 可以做你想让他们做的事情,要让他们做你想做的事会怎样?

嗯,基本上有两种方法可以做到:小规模和大规模。大规模版本将更改auto i = {a, b, c, d};的工作方式,以便它可以(基于某种方式)创建对表达式的可修改范围的引用。基于范围的for只会使用其现有的auto推导机制来获取它。

这当然不是入门者。该定义已经具有明确的含义:它创建这些表达式的副本的不可修改列表,而不是对其结果的引用。更改它将是一个重大变化。

一个小规模的变化是根据范围表达式是否为括号初始列表,对基于范围的for进行修改。现在,由于此类范围及其迭代器已被编译器为基于范围的for生成的代码所掩埋,因此您将不会遇到那么多的向后兼容性问题。因此,您可以制定一条规则,如果您的范围声明定义了一个非{const引用变量,而range-expression是一个括号初始列表,那么您可以执行一些不同的推论机制。

这里最大的问题是,它是一个完整而完整的黑客。如果执行for(auto &i : {a, b, c d})很有用,那么获得基于范围的for循环的相同类型的范围 outside 可能是有用的。按照目前的情况,关于auto减去括号初始列表的规则到处都是一致的。基于范围的for之所以获得其功能,仅仅是因为它使用了auto推导。

C ++最后需要做的是更多不一致

根据C ++ 20 adding an init-statement to range-for,这非常重要。这两件事应该是等效的:

for(auto &i : {a, b, c, d})
for(auto &&rng = {a, b, c, d}; auto &i: rng)

但是,如果仅基于range-expression和range-statement更改规则,则不会。 rng将根据现有的auto规则推导,从而使auto &i无法正常工作。与其他地方的同一条语句不同,拥有改变范围for的初始化语句行为方式的特例规则将变得更加不一致。

此外,编写一个通用的reference_range函数并接受非const可变参数参考(相同类型)并在其上返回某种随机访问范围并不是很困难。它将在所有地方均等地工作,而不会出现兼容性问题。

因此,让我们避免尝试建立旨在将对象初始化为通用的“材料清单”工具的语法结构。