thing
contains 2 vectors, one of foo
and one of bar
.
The bar
instances contain references to the foos
- the potentially dangling ones.
The foo
vector is filled precisely once, in things
's constructor initializer list, and the bar
vector is filled precisely once in things
's constructor body.
main()
holds a std::vector<thing>
but this vector is filled incrementally without .reserve()
, and is therefore periodically reallocating.
I am struggling to reproduce it in the minimal example below, but in the more heavyweight complete code the f1
and f2
references trigger the address sanitizer with "use after free".
I find this "slightly" surprising, because yes, the "direct members" of std::vector<foo>
in thing (ie the start_ptr, size, capacity), they get realloc'd when things
in main()
grows. But I would have thought that the "heap resource" of foos
could (?) stay the same when the std::vector<thing>
get's realloc'd because there is no need to move them.
Is the answer here, that: "Yes the foo
heap objects may not move when things
realloc's, but this is by no means guaranteed and that's why I am getting inconsistent results"?
What exactly is and isn't guaranteed here that I can rely on?
#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;
}