父容器重新分配时嵌套向量中的悬挂引用
dangling reference in nested vector when parent container reallocates
thing
包含 2 个向量,foo
之一和 bar
.
之一
bar
个实例包含对 foos
的引用 - 潜在的悬挂实例。
foo
向量在things
的构造函数初始化列表中被精确填充一次,bar
向量在things
的构造函数中被精确填充一次正文
main()
持有 std::vector<thing>
,但此向量在没有 .reserve()
的情况下递增填充,因此会定期重新分配。
我正在努力在下面的最小示例中重现它,但在更重量级的完整代码中,f1
和 f2
引用通过“释放后使用”触发地址清理程序。
我发现这“有点”令人惊讶,因为是的,std::vector<foo>
中的“直接成员”(即 start_ptr、大小、容量),当 things
在 main()
增长。但是我认为 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::vector
s,C++17之后就是这样。 thing
的自动生成的移动构造函数因此也应该符合条件。
考虑到这些,您发布的代码是正确的。由于我们没有看到所有代码,因此其他地方一定存在与您拥有的代码交互的问题。也许您在实际代码中有一个用户定义的移动构造函数,您没有标记为 noexcept
,或者您 push_back 到 foo 向量之一。
此外,reserve
调用是 no-op:foos.reserve(bars.size());
。 bars.size()
这里是0。你是说bars.reserve(foos.size());
吗?
thing
包含 2 个向量,foo
之一和 bar
.
bar
个实例包含对 foos
的引用 - 潜在的悬挂实例。
foo
向量在things
的构造函数初始化列表中被精确填充一次,bar
向量在things
的构造函数中被精确填充一次正文
main()
持有 std::vector<thing>
,但此向量在没有 .reserve()
的情况下递增填充,因此会定期重新分配。
我正在努力在下面的最小示例中重现它,但在更重量级的完整代码中,f1
和 f2
引用通过“释放后使用”触发地址清理程序。
我发现这“有点”令人惊讶,因为是的,std::vector<foo>
中的“直接成员”(即 start_ptr、大小、容量),当 things
在 main()
增长。但是我认为 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::vector
s,C++17之后就是这样。 thing
的自动生成的移动构造函数因此也应该符合条件。
考虑到这些,您发布的代码是正确的。由于我们没有看到所有代码,因此其他地方一定存在与您拥有的代码交互的问题。也许您在实际代码中有一个用户定义的移动构造函数,您没有标记为 noexcept
,或者您 push_back 到 foo 向量之一。
此外,reserve
调用是 no-op:foos.reserve(bars.size());
。 bars.size()
这里是0。你是说bars.reserve(foos.size());
吗?