std::unique_ptr, pimpl and object lifetime

Viewed 97

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 object m_owner to 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 ?

1 Answers

Yes, the lifetime of the object that m_owner refers to has already ended and it's destructor call completed when m_owner->cleanup(); is called. The call is therefore UB.

Related