向量中的向量强制转换出现未定义行为

20

为什么这段代码会写入看似未初始化的整数呢?

#include <iostream>
#include <vector>
using namespace std;


int main()
{
    for (int i : vector<vector<int>>{{77, 777, 7777}}[0])
        cout << i << ' ';
}

我期望输出结果为77 777 7777

这段代码是不是应该没有定义?

3个回答

18

vector<vector<int>>{{77, 777, 7777}} 是一个临时变量,如果在 ranged-for 中使用 vector<vector<int>>{{77, 777, 7777}}[0] 将会产生未定义的行为。

您应该先创建一个变量,例如

#include <iostream>
#include <vector>
using namespace std;


int main()
{
    auto v = vector<vector<int>>{{77, 777, 7777}};
    for(int i: v[0])
        cout << i << ' ';
}

另外,如果您使用Clang 10.0.0,则会对此行为发出警告。

警告:指针背后的对象将在完整表达式结束时被销毁[-Wdangling-gsl] vector>{{77, 777, 7777}}[0]


4
为了防止这种不良实践蔓延,请使用using std::vector而非using namespace std; - infinitezero

11

这是因为在进入循环之前,您正在迭代的向量将被销毁。

通常会发生以下情况:

auto&& range = vector<vector<int>>{{77, 777, 7777}}[0];
auto&& first = std::begin(range);
auto&& last = std::end(range);
for(; first != last; ++first)
{
    int i = *first;
    // the rest of the loop
}

问题始于第一行,因为它按照以下方式进行评估:

  1. 首先使用给定的参数构造向量的向量,并且该向量成为临时向量,因为它没有名称。

  2. 然后范围引用被绑定到下标向量,只有在包含它的向量有效的情况下才有效。

  3. 一旦遇到分号,临时向量就会被销毁,在其析构函数中,它将销毁并释放任何存储的向量,包括下标向量。

  4. 最终你得到了一个对已销毁向量的引用,这个引用将被迭代。

为了避免这个问题,有两个解决方案:


  1. Declare the vector before the loop so it lasts until its scope ends which includes the loop.

  2. C++20 comes with an init statement which is provided to solve these problems and is better than the first approach if you want the vector to be destroyed after the loop immediately:

    for (vector<vector<int>> vec{{77, 777, 7777}}; int i : vec[0])
    {
    }
    

这不是“通常”发生的事情。这种确切的行为(加上适当的作用域和命名考虑)是由标准规定的,受到 as-if 规则的约束。 - Konrad Rudolph
我指的是生命周期。即使您手动编写相同的代码,您只能保证获得所需的行为,并且编译器会尽可能地进行优化。 - dev65
今天我了解到了 C++20 中的声明范围 for 语法。不确定是开心还是难过。 - Asteroids With Wings

6
vector<vector<int>>{{77, 777, 7777}}[0]

我认为这是悬挂的。

尽管range-for的定义保证冒号右侧的对象在循环期间保持“活着”,但你仍然在对一个临时对象进行下标操作。只有下标操作的结果被保留,而该结果是一个引用,实际的向量(vector)不能在声明它的完整表达式之后继续存在。这并不能描述整个循环。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接