struct Foo {
int id;
};
struct Inner {
Inner(Outer* outer) : outer_(outer) {}
~Inner() {
int id = outer_->foo_->id;
}
Outer* outer_;
};
struct Outer {
// Caller guarantees that |foo| will be alive longer than Outer.
Outer(Foo* foo) {
foo_ = foo;
}
Inner inner_;
CheckedPtr<Foo> foo_;
};
In the example above, |Outer::foo_| field is destructed first - at that time CheckedPtr's destructor decrements BackupRefPtr's ref-count and (because the decrement might have led to a |free|) has to make sure that this CheckedPtr cannot be dereferenced (by setting it to |nullptr|). Later |Outer::inner_| field is destructed and this may lead to dereferencing the |Outer:foo_| pointer field - this would have worked with raw pointers but crashes with CheckedPtr.
One fix would be to move all the rewritten pointer fields to the top (ensure that CheckedPtr fields are declared before any other non-POD fields). This can be seen as an additional ergonomics burden of the BackupRefPtr approach. In theory, the rewriter could reorder field declarations, but in practice this is tricky, because comments need to be also moved, but 1) comments are already stripped when working with the AST and 2) sometimes a single comment applies to multiple fields and 3) private:/protected:/etc aspects need to be preserved.
Another fix would be to avoid using partially-destructed objects (e.g. Outer where some fields have been destructed and some haven't). This also seems difficult in practice (even if all the typical guidelines are followed - e.g. avoiding calling virtual methods in destructors). I wonder what the C++ spec has to say about accessing raw pointers (or other POD fields) that have been "destructed" - maybe this is somehow Undefined Behavior even without CheckedPtr in the picture?
Are there any other ways to address this? Maybe we can hope that fixing the field order in a handful of cases will be sufficient?
At any rate, we probably should document this gotcha in //base/memory/checked_ptr.md - I'll try to put together a CL with some doc tweaks