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;
}
CodePudding user response:
What you are guaranteed is, upon moving a std::vector
, no iterator, pointer or reference will be invalidated. This would apply to the vectors inside thing
. See notes in https://en.cppreference.com/w/cpp/container/vector/vector
When a std::vector
grows, all iterators, pointers and references to it become invalid. So if you had a reference to a thing
, those would be blown away, but you do not have that, so we are good.
While a std::vector
grows, it will move the previous elements to the new allocation if the contained type has a noexcept
move constructor. For std::vector
s, this is the case after C 17. The automatically generated move constructor of thing
therefore should also qualify.
Considering these, the code you have posted is correct. As we do not see all the code, there must be an issue somewhere else that interacts with the code you have. Perhaps you have a user defined move constructor in the real code that you did not mark as noexcept
, or you push_back to one of the foo vectors.
Also, the reserve
call is a no-op: foos.reserve(bars.size());
. bars.size()
here is 0. Did you mean bars.reserve(foos.size());
?