向std::vector添加类字段时出现奇怪的行为

32

我发现在以下情况下有一些非常奇怪的行为(在clang和GCC上)。我有一个向量nodes,其中包含一个元素,即类Node 的一个实例。然后我调用 nodes[0] 上的函数来向向量添加一个新的Node。当添加新的 Node 时,调用对象的字段被重置了!但是,一旦函数完成,它们似乎又恢复了正常。

我认为这是一个最小可重现的示例:

#include <iostream>
#include <vector>

using namespace std;

struct Node;
vector<Node> nodes;

struct Node{
    int X;
    void set(){
        X = 3;
        cout << "Before, X = " << X << endl;
        nodes.push_back(Node());
        cout << "After, X = " << X << endl;
    }
};

int main() {
    nodes = vector<Node>();
    nodes.push_back(Node());

    nodes[0].set();
    cout << "Finally, X = " << nodes[0].X << endl;
}

输出的内容

Before, X = 3
After, X = 0
Finally, X = 3

虽然你期望通过该过程X保持不变。

我尝试了其他一些方法:

  • 如果我删除将Node添加到set()内部的代码行,则每次输出的X都为3。
  • 如果我创建一个新的Node并在其上调用它(Node p = nodes[0]),则输出为3、3、3。
  • 如果我创建一个引用Node并在其上调用它(Node &p = nodes[0]),则输出为3、0、0(也许是因为向量重新调整大小时丢失了引用?)

这是由于某种原因导致的未定义行为吗? 为什么?


4
查看 https://en.cppreference.com/w/cpp/container/vector/push_back 。如果在调用 set() 之前对向量调用了 reserve(2),那么这将是定义行为。但编写一个类似于 set 的函数,要求用户在调用它之前适当地 reserve 足够的大小以避免未定义行为是不好的设计,因此不要这样做。 - JohnFilleau
2个回答

40

你的代码存在未定义行为。

void set(){
    X = 3;
    cout << "Before, X = " << X << endl;
    nodes.push_back(Node());
    cout << "After, X = " << X << endl;
}

访问X实际上是this->X,而this是指向向量成员的指针。当你执行nodes.push_back(Node());时,你向向量添加一个新元素,并且该过程重新分配内存,这将使向量中所有迭代器、指针和引用无效。这意味着

cout << "After, X = " << X << endl;

使用了一个不再有效的 this


调用push_back已经成为未定义的行为了吗(因为我们在带有失效this的成员函数中),或者是第一次使用this指针发生了UB?我们是否可以像return 42;那样做呢? - n314159
3
nodesNode 实例无关,因此调用 push_back 不会产生未定义行为。未定义行为发生在使用无效指针之后。 - NathanOliver
@n314159 一个很好的概念化方法是想象一个函数 void set(Node* this),传递无效指针或在函数中使用 free() 并不会导致未定义行为。我不确定,但我想即使你不使用 this 并且该方法不是虚拟的,((Node*) nullptr)->set() 也是被定义的。 - DutChen18
我认为 ((Node *) nullptr)->set() 不太好,因为这会对一个空指针进行解引用操作(当你将其等效地写成 (*((Node *) nullptr)).set(); 时,你会更清楚地看到这一点)。 - n314159
.push_back() 可能 会使所有指针、引用和迭代器失效。.capacity() == .size() 吗?是的,看起来是这样。 - Deduplicator

15
nodes.push_back(Node());

重新分配vector,因此更改nodes [0]的地址,但this未更新。
尝试将set方法替换为以下代码:

    void set(){
        X = 3;
        cout << "Before, X = " << X << endl;
        cout << "Before, this = " << this << endl;
        cout << "Before, &nodes[0] = " << &nodes[0] << endl;
        nodes.push_back(Node());
        cout << "After, X = " << X << endl;
        cout << "After, this = " << this << endl;
        cout << "After, &nodes[0] = " << &nodes[0] << endl;
    }

请注意,在调用push_back后,&nodes[0]与之前不同。

-fsanitize=address可以捕获这个错误,如果你还使用-g编译,它甚至可以告诉你释放内存的代码行数。


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