Skip to content

Pimpl for Small Classes

The familiar solution for thick value classes that want to preserve binary compatibility is to use the pimpl pattern (private implementation), also known as d-pointer (pointer to data). In future versions of our class, we can freely change the contents of the pimpl (i.e. adding, removing, and/or modifying data members) but the binary compatibility of the public class gets preserved.

There’s a minor variation of the pimpl pattern that can enable some performance improvements by not allocating the private data in all cases. The idea is pretty simple: move (some of) the data members in the public class, while still keeping a d-pointer data member.

This optimization makes a lot of sense, if the class that we’re pimpling does not hold a lot of state. In general, there are multiple reasons why one may want to conditionally allocate the private data:

  1. maybe we don’t need it at all at the moment (again, class with a small state). We want to have room for it “just in case” we need to “grow” our class sometime in the future and want to keep binary compatibility (so we can’t just add new data members to it) and/or
  2. because the pimpl stores data that it’s only seldom needed. In the “common case,” we can spare the allocation for that data.

The second scenario isn’t really interesting; it’s a normal lazy allocation.

Let’s, instead, assume we’re in scenario number 1, in which the d-pointer is unused right now but there as room for future growth. Let’s further assume that we are not also using implicit sharing or any other complications of that nature.

In this scenario, a pimpled class will look something like this:

class WiperSettings {
public:
  enum class Status : int8_t { Off, Slow, Medium, Fast };

  EXPORT WiperSettings();
  EXPORT ~WiperSettings();
  EXPORT WiperSettings::WiperSettings(const WiperSettings &);
  EXPORT WiperSettings &operator=(const WiperSettings &);

  inline WiperSettings(WiperSettings &&) noexcept;
  inline WiperSettings &operator=(WiperSettings &&) noexcept;
  inline void swap(WiperSettings &) noexcept);

  EXPORT Status getStatus() const;
  EXPORT void setStatus(Status);

private:
   Status s;  // current inline state, small
   class WiperSettingsPrivate *d;  // dpointer for future expansion
};

In the above snippet, I am highlighting which members are exported and out of line and which ones are inline. We must be extra careful with inline functions, as, generally speaking, their code will get compiled into the calling code. The consequence of this is that we can’t change their semantics while keeping binary compatibility.

Let’s now compare the implementation of the special member functions in the current version of the class (“right now”) with the implementation “in the future,” when the class will need to make use of the d-pointer because it needs to grow.

SMF Right now (no dpointer used) In the future (use dpointer) Remarks
Default ctor Initialize the status; initialize the d-pointer to nullptr. Although the d-pointer is unused at this moment, we’re going to default other SMFs and we don’t want UB to be triggered by reading uninitialized data. Initialize the status and allocate the pimpl. In the future, it will need to change behavior and have access to the complete private class; so, it must be exported and out of line.
Dtor Default implementation (destroy the members). Destroy the members and deallocate the pimpl. Same as above.
Copy ctor Default implementation (member-wise copies). Allocate a new pimpl by copying from the existing one. Same as above.
Copy assignment Default implementation (member-wise assignments). Copy and swap, or a more ad-hoc strategy. While copy and swap is universal and could potentially be inlined, it’s not necessarily the most efficient implementation. If the class is extended, one may need to switch away from copy and swap to a much more efficient strategy. The allocation cost in the copy is going to dwarf the cost of the out of line call anyway.
Move ctor Move status and d from the source and reset other.d to nullptr, even if we’re not using the d-pointer right now (⚠️). See below. ←←← Has to be the same — it’s inline! The move constructor can and should be fully inline. Making it out of line introduces overhead for no good reason.
Move assignment Move and swap. ←←← Has to be the same — it’s inline! Fully inline as well. Could be implemented in terms of pure swap, but in Qt we prefer the determinism of move and swap when we cannot be sure that a class only holds memory (see here).
Swap Swap status and swap d, even if we’re not using the d-pointer right now. ←←← Has to be the same — it’s inline! Fully inline as well.

There is one very important thing to stress in the above table: “right now” the inline move constructor must move the d-pointer over and reset other.d to nullptr, even if we’re not using them at all. In other words, the post-condition of the inline move constructor must be this->d is the old value of other.d and other.d is nullptr. This means that the move constructor cannot be defaulted, even if “right now” defaulting it would do the correct thing.

But why is this? The explaination is a bit tricky, so stay with me.

This postcondition is necessary in order to allow the future version of the class to correctly handle destruction/assignment of moved-from instances while keeping the BC promise.

The point is that a move operation that happens in code compiled against the today version of the class will correctly handle an object produced by the future version of the class (remember, in the future the d-pointer is actually being used). Since the move operation is fully inline, it does not get recompiled when we upgrade to the future version of the class. So we must already prepare todayfor an ownership transfer of the d-pointer!

We’re at now, now.


So far, so good.

There’s a further optimization possible. In the current design, each instance has to pay for the storage space of a d-pointer even if it’s not using it all. Is it OK to have a class that consumes more space (in the example above it could be twice the size of a pointer, due to padding), just for the sake of binary compatibility? What if this is a super-popular class and you may realistically create thousands or millions of such objects? Are you even sure you’re going to use the d-pointer “in the future”?

We can trade size for a small refactoring: “in the future,” should we decide to pimpl the class, we’ll then fold inside the pimpl the contents of the state currently stored directly in the public class itself. This refactoring lets us change the implementation from a record type (store both inline state and d-pointer) to an alternative type (store either inline state or the d-pointer).


In C++, the alternative type is, of course, std::variant<InlineState, Private *>. Here, it would be the easiest choice but not the ideal type to use, as it would waste space for the index of the current active alternative, where we “statically” know which alternative is active: “right now” it’s the inline state, “in the future” the pimpl.

So, a union it is, and no extra space required:

class WiperSettings {
public:
  ~~~
private:
   union {    // anonymous
      Status s;
      WiperSettingsPrivate *d;
   };
};

Let’s do the exercise again. What do we do in each special member function now?

SMF Right now (no dpointer used) In the future (use dpointer)
Default ctor Initialize s. Initialize d by allocating the pimpl.
Dtor Default implementation (destroy the members). Deallocate d.
Copy ctor Default implementation (member-wise copies). Allocate a new pimpl by copying from the existing one.
Copy assignment Default implementation (member-wise assignments). Copy and swap, or a more ad-hoc strategy.
Move ctor ⚠️⚠️⚠️ ←←← Has to be the same, it’s inline!
Move assignment Move and swap. ←←← Has to be the same, it’s inline!
Swap ⚠️⚠️⚠️ ←←← Has to be the same, it’s inline!

Now we have a problem: we can no longer meaningfully implement an inline move constructor and we cannot implement swap. (And therefore we can’t implement move and swap — that uses both). Why is that?

Remember: the move constructor must carry the d-pointer from the source object (other) onto *this and reset other.d to nullptr, if we want to preserve binary compatibility. (other may come from “the future.” The move constructor is inlined “right now“).

With this necessary post-condition, the straightforward implementation doesn’t work:

WiperSettings::WiperSettings(WiperSettings &&other) noexcept
    : d(std::exchange(other.d, nullptr)) {}

This move constructor is reading from other.d “right now”, but that union field is not active! That’s completely illegal in C++ (file under “UB that works in pratice”).

The same problem applies to an inline swap — which member of the union do we swap? Right now we’re supposed to swap s, but that makes it binary incompatible with an object from the future, where d is in use.


A workaround here is possible, but only if the inline state is trivial (so an enum is fine, an int is fine, a struct containing enum+int is fine; but a QString is not fine). The workaround simply consists of making the union not anonymous and moving/swapping the entire union.

class WiperSettings {
public:
  ~~~
private:
   union U {
      Status s;
      WiperSettingsPrivate *d;
   } u;
};

The move constructor and swap function then become:

WiperSettings::WiperSettings(WiperSettings &&other) noexcept
    : u(other.u)
{
    other.u.d = nullptr; 
}

void WiperSettings::swap(WiperSettings &other) noexcept
{
    using std:swap; swap(u, other.u);
}

The trick here is that, by moving/swapping the union (as opposed as a union’s member), we make C++ responsible for making correct that the active field is the one actually moved. It’s no longer our problem!

This is also the reason for imposing triviality for the inline state; otherwise, the union’s special member functions are not necessarily generated (whoops!) and we cannot switch the active member of the union (in the move constructor, we enable other.u.d) without first of calling an explicit destructor on the currently active union member; and we don’t know which one is enabled.

You’ll find this pattern soon deployed in a Qt version near you, for instance, in the new QPermission datatypes. Many thanks to Marc Mutz for implementing the above design in many commits to Qt (e.g. here, here).

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

2 thoughts on “Pimpl for Small Classes”

  1. Hi Giuseppe,

    have you a simple test on github ?I should test/study this pattern.

    Thanks

Leave a Reply

Your email address will not be published. Required fields are marked *