父容器重新分配时嵌套向量中的悬挂引用

dangling reference in nested vector when parent container reallocates

thing 包含 2 个向量,foo 之一和 bar.

之一

bar 个实例包含对 foos 的引用 - 潜在的悬挂实例。

foo向量在things的构造函数初始化列表中被精确填充一次,bar向量在things的构造函数中被精确填充一次正文

main() 持有 std::vector<thing>,但此向量在没有 .reserve() 的情况下递增填充,因此会定期重新分配。

我正在努力在下面的最小示例中重现它,但在更重量级的完整代码中,f1f2 引用通过“释放后使用”触发地址清理程序。

我发现这“有点”令人惊讶,因为是的,std::vector<foo> 中的“直接成员”(即 start_ptr、大小、容量),当 thingsmain() 增长。但是我认为 foos 的“堆资源”可以(?)在 std::vector<thing> get 重新分配时保持不变,因为不需要移动它们。

这里的答案是:“是的,当 things 重新分配时,foo 堆对象可能不会移动,但这绝不是保证,这就是我得到不一致结果的原因”?

我可以信赖的具体内容是什么?

#include <iostream>
#include <vector>

struct foo {
    int x;
    int y;
    // more stuff
    friend std::ostream& operator<<(std::ostream& os, const foo& f) {
        return os << "[" << f.x << "," << f.y << "]";
    }
};

struct bar {
    foo& f1; // dangerous reference
    foo& f2; // dangerous reference
    // more stuff
    bar(foo& f1_, foo& f2_) : f1(f1_), f2(f2_) {}

    friend std::ostream& operator<<(std::ostream& os, const bar& b) {
        return os << b.f1 << "=>" << b.f2 << "  ";
    }
};

struct thing {
    std::vector<foo> foos;
    std::vector<bar> bars;

    explicit thing(std::vector<foo> foos_) : foos(std::move(foos_)) {
        bars.reserve(foos.size());
        for (auto i = 0UL; i != foos.size(); ++i) {
            bars.emplace_back(foos[i], foos[(i + 1) % foos.size()]); // last one links back to start
        }
    }

    friend std::ostream& operator<<(std::ostream& os, const thing& t) {
        for (const auto& f: t.foos) os << f;
        os << "  |  ";
        for (const auto& b: t.bars) os << b;
        return os << "\n";
    }
};

int main() {
    std::vector<thing> things;
    things.push_back(thing({{1, 2}, {3, 4}, {5, 6}}));
    std::cout << &things[0] << std::endl;
    for (const auto& t: things) std::cout << t;
    
    things.push_back(thing({{1, 2}, {3, 4}, {5, 6}, {7, 8}}));
    std::cout << &things[0] << std::endl;
    for (const auto& t: things) std::cout << t;
    
    things.push_back(thing({{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}}));
    std::cout << &things[0] << std::endl;
    for (const auto& t: things) std::cout << t;
    
    things.push_back(thing({{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}, {11, 12}}));
    std::cout << &things[0] << std::endl;
    for (const auto& t: things) std::cout << t;
}

您可以保证,在移动 std::vector 后,不会使迭代器、指针或引用失效。这将适用于 thing 内的矢量。请参阅 https://en.cppreference.com/w/cpp/container/vector/vector

中的注释

std::vector 增长时,所有迭代器、指针和对它的引用都会失效。因此,如果您有对 thing 的引用,那么它们就会被吹走,但您没有,所以我们很好。

std::vector 增长时,如果 包含的类型具有 noexcept 移动构造函数,它会将先前的元素移动到新分配 。对于std::vectors,C++17之后就是这样。 thing 的自动生成的移动构造函数因此也应该符合条件。

考虑到这些,您发布的代码是正确的。由于我们没有看到所有代码,因此其他地方一定存在与您拥有的代码交互的问题。也许您在实际代码中有一个用户定义的移动构造函数,您没有标记为 noexcept,或者您 push_back 到 foo 向量之一。

此外,reserve 调用是 no-op:foos.reserve(bars.size());bars.size()这里是0。你是说bars.reserve(foos.size());吗?