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;
}