将另一个实例添加到同一向量后,对成员变量的引用中断

Reference to member variable broken after adding another instance to the same vector

考虑以下代码:

struct Point {
    int x;
    int& xref = x;
};

int main() {
    std::vector<Point> points;
    for (int i = 0; i < 2; i++) {
        Point p;
        p.x = i;
        points.push_back(p);
        assert (points[0].x == points[0].xref);
    }

    return 0;
}

第二次迭代断言失败。为什么?

我正在使用 GNU C++ 编译器。我测试过的所有标准都会出现问题:

每个元素的 xref 似乎引用了最后添加的元素的 x 而不是它自己的,如下所示:https://pastebin.com/H4wCszxp

当你把一个对象推入一个向量时,你就做了一个副本†。

当您复制引用(即成员)时,新引用将引用与复制来源相同的对象。因此,如果您将 Point p 复制到 Point copy,那么 copy::xref 将引用 p.x,因为那是 p.xref 所引用的内容。

因此,向量中的所有 Point 对象都是在循环范围内构造的 Point p 自动变量的副本。因此,向量中的所有这些对象都引用作为自动变量 p 成员的 int 对象。 None 其中指的是他们自己的 x 成员。

在第一次迭代期间这很好,因为 points[0].xref 指的是现有的 p.x,它也具有与 points[0].x 相同的值。但在该迭代结束时,自动变量 ppoints[0].xref 引用其成员)被销毁。此时 points[0].xref 是一个不再引用有效对象的悬挂引用。在下一次迭代中使用参考。使用悬空引用具有未定义的行为。


如果你想访问this对象,那么使用this指针。如果要存储对对象的引用,则不要期望该引用的副本引用另一个对象。避免将引用(或指针)存储到生命周期短于持有引用的对象的对象。


† ... 或在压入右值时移动。您不推送右值,并且副本与 Point) 的移动完全相同,因此这是无关紧要的细节。

发生这种情况是因为当将点插入向量时它会被复制,并且引用指向原始值。 例如。这项工作很好:

int main() {
    Point p;
    p.x = 100;
    assert (p.x == p.xref);

  return 0;
}

我怀疑您可能对声明 int& xref = x; 的工作方式有一点误解。这只是一个默认的初始化。

  • 它不保证 xref 它总是被构造为引用 x。默认复制构造函数将复制源实例的外部参照。
  • 此外,您可以使用初始化列表更改外部参照绑定。

我将首先使用您所看到的简化版本进行说明,然后使用您的代码的修改版本。最后,我将对您的 Point 对象进行调整,使其按照您的预期运行。

int main()
{
  Point p;
  p.x = 1;
  std::cout << &p.x << "(" << p.x << ") " << &p.xref << "(" << p.xref << ")\n";

  Point p2 = p;
  std::cout << &p2.x << "(" << p2.x << ") " << &p2.xref << "(" << p2.xref << ")\n";
  p2.x = 2;
  std::cout << &p2.x << "(" << p2.x << ") " << &p2.xref << "(" << p2.xref << ")\n";

  Point p3 {3, p2.x};
  std::cout << &p3.x << "(" << p3.x << ") " << &p3.xref << "(" << p3.xref << ")\n";
  return 0;
}

示例输出:

//The in value of p.x and p.xref are in the same location
0x7ffde0450670(1) 0x7ffde0450670(1)
//p2 is initialised as a copy of p
//So p2.x has its own location but value is copied
//and p2.xref refers to the same location as p.xref (and hence p.x)
0x7ffde0450680(1) 0x7ffde0450670(1)
//Therefore updating p2.x doesn't affect p2.xref
0x7ffde0450680(2) 0x7ffde0450670(1)
//All members of p3 are initialised with initialiser list
//p3.x has its own location
//p3.xref is assigned location of p2.x
0x7ffde0450690(3) 0x7ffde0450680(2)

图形上看起来像这样:

p-----> +-------------------------+
     +->| x = 1                   |
     |  | xref = 0x7ffde0450670 --|--+
     |  +-------------------------+  |
     |-------------------------------+
     |
     +--------------------------------+
p2-----> +-------------------------+  |
     +-> | x = 2                   |  |
     |   | xref = 0x7ffde0450670 --|--+
     |   +-------------------------+  
     +--------------------------------+
p3-----> +-------------------------+  |
         | x = 3                   |  |
         | xref = 0x7ffde0450680 --|--+
         +-------------------------+  

稍微修改您的代码:

std::vector<Point> points;
for (int i = 0; i < 2; i++) {
    Point p;
    p.x = i;
    points.push_back(p);
    std::cout << i << " " << &p << "\n";
    std::cout << &p.x << "(" << p.x << ") " << &p.xref << "(" << p.xref << ")\n";
    std::cout << &points[0].x << "(" << points[0].x << ") " << &points[0].xref << "(" << points[0].xref << ")\n";
}

示例输出:

0 0x7fffa80e0840
0x7fffa80e0840(0) 0x7fffa80e0840(0)
0x55bd4ff52c30(0) 0x7fffa80e0840(0)
//On each iteration p is in the same location
1 0x7fffa80e0840
//So p.x is in the same location
0x7fffa80e0840(1) 0x7fffa80e0840(1)
//And the vector elements are initialised with p
//So each element's xref is the same as p.x and p.xref
0x55bd4ff52c50(0) 0x7fffa80e0840(1)

最后,通过对 Point 构造函数进行一些控制,您可以使其按预期运行。

struct Point {
  int x;
  int& xref = x;
  Point() = default;
  Point(const Point& p): x(p.x) {}
};

以上代码执行以下操作:

  • 它将默认的复制构造函数替换为仅从源复制 x 值的复制构造函数。因此 xref 保留其默认初始化。
  • 由于定义了您自己的构造函数,默认构造函数被删除。所以现在你需要显式地声明它;幸运的是,您可以指定默认实现就足够了。