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:
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 likereset
orassign
. 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.Don't implement move semantics for
Thing
at all. Every move is a full copy.Allow
m_pimpl
to be null. Every public member function ofThing
must check for this case before using*m_pimpl
. Perhaps a default-initializedThing
also has nullm_pimpl
.Make
m_pimpl
astd::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 tovoid 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.]