The following example compiles with both gcc 11 on Linux (GNU STL) and clang 12 on FreeBSD (Clang STL). On Linux, it runs and prints values 1 and 2. On FreeBSD, it prints value 1 and then crashes with a SEGV. I don't quite understand the object lifetimes -- so the whole thing may be UB and the runtime behavior might not be relevant. I do know that the implementation of std::unique_ptr between those two STLs differs in an important way: Clang STL resets the internal pointer of a std::unique_ptr to nullptr at the start of the destructor, while GNU STL leaves the pointer alone.
#include <iostream>
#include <memory>
struct C {
struct Private {
C* m_owner;
int m_x;
Private(C* owner) : m_owner(owner), m_x(0) {}
~Private() { m_owner->cleanup(); }
void cleanup() { std::cout << "Private x=" << ++m_x << '\n'; }
};
std::unique_ptr<Private> d;
C() { d = std::make_unique<Private>(this); }
~C() = default;
void cleanup() { d->cleanup(); }
};
int main(int argc, char **argv)
{
C c;
c.cleanup(); // For display purposes, print 1
return 0; // Destructors called, print 2
}
Output on FreeBSD:
Private x=1
Segmentation fault (core dumped)
and a snippet of backtrace:
* thread #1, name = 'a.out', stop reason = signal SIGSEGV: invalid address (fault address: 0x8)
frame #0: 0x00000000002032b4 a.out`C::Private::cleanup() + 52
a.out`C::Private::cleanup:
-> 0x2032b4 <+52>: movl 0x8(%rax), %esi
My reason for thinking this might be UB is this:
- at
return 0,c's lifetime is ending. - the destructor
~C()runs. Once the body (defaulted) of the destructor is done, the lifetime of the object is over and using that object is UB. - now the destructors for sub-objects (member-objects?) of the object run.
- the destructor
~std::unique_ptr<Private>runs. It runs the destructor for the held object. - the destructor
~Private()uses a pointer to a no-longer-alive objectm_ownerto call a member function.
I'd appreciate an answer that points out if this understanding of object lifetimes is correct.
If it's not UB, then there's a separate quality-of-implementation issue (or I should check the d-pointer before calling methods on it, but that seems a bit derpy for a pimpl; then we get if(d)d->cleanup() which is needed with one STL implementation and which is a useless check in another).
In the interest of posing a single question: does this code exhibit UB in the statement m_owner->cleanup() (line 9) during the destruction of object c ?