1

Suppose I'm writing a C++ class with the PImpl idiom for the usual reasons of providing a stable ABI and/or reducing #include dependencies. I want the class to have value semantics: modifying a copy of the object has no visible effect on the original object. Assume doing a semantic copy (copying the implementation members) is not "fast" in some meaningful sense.

class Thing {
public:
    Thing();
    Thing(const Thing&);
    Thing(Thing&&);
    Thing& operator=(const Thing&);
    Thing& operator=(Thing&&);
    ~Thing();
    // Various accessor and mutator functions...
private:
    class Impl;
    std::unique_ptr<Impl> m_pimpl;
};

A tempting implementation of the move constructor would be

Thing::Thing(Thing&&) = default;

which is essentially the same as

Thing::Thing(Thing&& src) : m_pimpl(std::move(src.m_pimpl)) {}

But either of these leaves the moved-from Thing with a null m_pimpl member. Otherwise, I'd like to have a non-null m_pimpl as a class invariant. If any member function of Thing doesn't check for and deal with a null m_pimpl, that opens up accidental undefined behavior. Herb Sutter's "Move, Simply" post makes a good argument that allowing that invariant violation for moved-from objects is bad design.

The options I can think of are:

  1. A moved-from Thing is in a special restricted state. The only public member functions which can be called on it are the destructor, the assignment operators, and maybe some other functions with names like reset or assign. All other public member functions have a precondition that the object is not in this moved-from state. This is exactly what I read Sutter's article as warning against.

  2. Don't implement move semantics for Thing at all. Every move is a full copy.

  3. Allow m_pimpl to be null. Every public member function of Thing must check for this case before using *m_pimpl. Perhaps a default-initialized Thing also has null m_pimpl.

  4. Make m_pimpl a std::shared_ptr<Thing::Impl> which is never null, and implement copy on write. Non-mutating functions can simply access *m_pimpl. Mutating member functions which use the existing state rather than replace it begin with a call to

     void Thing::copy_on_write() {
         if (m_pimpl.use_count() > 1)
             m_pimpl = std::make_shared<Impl>(*m_pimpl);
     }
    

All of these options have some benefits and drawbacks, it seems. What solutions, above or otherwise, work well and with a clean, maintainable design as a best practice for modern C++? What considerations are important when choosing how to implement the PImpl pattern?

[The Q&A "Object lifetime invariants vs. move semantics" is a more general version of this question, but I'm specifically asking about the PImpl use case.]

aschepler
  • 119
  • 3

1 Answers1

1

Option 2 is a good option if copying is a cheap operation anyway.

Option 3 is also a good option if having a NULL m_pimpl can be seen as a natural state of the object.

Besides those, there is also an option 5:

Thing::Thing(Thing&& src) : m_pimpl(std::make_unique<Impl>(std::move(*src.m_pimpl))) {}

Here, you create a new unique_ptr with a moved version of the Impl class. The Impl class can then decide for itself how to implement move semantics.

Bart van Ingen Schenau
  • 71,712
  • 20
  • 110
  • 179