Skip to content

Opaque Pointer Pattern in C++

Opaque Pointer Pattern

I’m doing a lot of code re-design and refactoring these days. One challenge is to change the implementation of a class or module while maintaining a stable interface for its clients. The ability to replace an old implementation with a new one without changing other parts of the code is extremely useful to ensure the correctness of the new implementation: it allows you to run integration tests with the new implementation and hunt down any bugs or regressions.

The opaque pointer design pattern is extremely useful in this situation. Maybe you’re familiar with it as pointer-to-implementation idiom or compiler firewall. It is also described as Bridge Pattern in the GoF book1.

This article walks you through the basics of the pattern and shows you how to implement it using std::unique_ptr.

What’s the Problem?

The basic problem is that C++ class declarations expose private details of the class. Private member functions and data members need to be declared in the header. Here’s an example for illustration:

// Point.h

class Point
{
public:
  Point(float x, float y);
  float x();
  float y();

private:
  float x_;
  float y_;
};

While users of this class don’t have direct access to the private data members x_ and y_, there is still a dependency: If you change the private implementation details of Point, all other compilation units that include Point.h know about the change and need to be re-compiled.

This only gets worse for more complex dependency chains, e.g., when there are dependencies to other classes internal to the module that need to be included. To a certain degree, this can be dealt with by using forward declarations. However, at the end of the day there is an information leak: Private implementation details are leaking to clients. This goes directly against the idea of information hiding.

The opaque pointer pattern helps to deal with this problem.

Hiding Behind a Pointer

The basic idea of the opaque pointer pattern is to hide the details of the implementation behind a pointer to an incomplete type. Picking up the Point example from above, a basic version might look like this:

// Point.h

class Point
{
public:
  Point(float x, float y);
  float x();
  float y();

private:
  struct Impl;  // forward-declaration of the implementation
  Impl* impl;   // pointer to implementation
};

As you can see, the header file only contains a forward-declaration and a pointer to an incomplete type. This doesn’t tell anything about how the point class actually stores its data. This decision is left to the implementation file:

// Point.cpp
// There's a problem in this example. Can you spot it?

#include "Point.h"

struct Point::Impl
{
  float x;
  float y;
};

Point::Point(float x, float y)
{
  impl = new Point::Impl();
  impl->x = x;
  impl->y = y;
}

float Point::x()
{
  return impl->x;
}

float Point::y()
{
  return impl->y;
}

Note how Point::Impl is only defined in the implementation file. You can now change the implementation of Point without changing the API defined in the header. Let’s say you prefer a std::array instead of two floats:

// Point.cpp
// There's still a problem in this example. Can you spot it?

#include "Point.h"

#include <array>

struct Point::Impl
{
  std::array<float, 2> data;
};

Point::Point(float x, float y)
{
  impl = new Point::Impl();
  impl->data[0] = x;
  impl->data[1] = y;
}

float Point::x()
{
  return impl->data[0];
}

float Point::y()
{
  return impl->data[1];
}

Voilà! Same interface, different implementation.

This is clearly a step forward in terms of information hiding. However, the example above has some problems, as already hinted at in the comments. I used a raw pointer to the Point::Impl struct, allocated in the Point constructor. However, I never delete it. Ouch!

Instead of fixing the above example by using manual memory management, let’s look into an alternative implementation using std::unique_ptr.

Opaque Pointer with std::unique_ptr

C++11 came with smart pointer classes that allow you to avoid memory management issues like the above. In particular, std::unique_ptr is a smart pointer has unique ownership semantics of the resource it manages and automatically takes care of object destruction. Let’s give this a try:

// Point.h

#include <memory>

class Point
{
public:
  Point(float x, float y);
  float x();
  float y();

private:
  struct Impl;                 // forward-declaration of the implementation
  std::unique_ptr<Impl> impl;  // pointer to implementation
};

Aww, too bad, doesn’t compile out of the box:

unique_ptr.h:66:19: error: invalid application of 'sizeof' to an incomplete type 'Point::Impl'
  static_assert(sizeof(_Tp) >= 0, "cannot delete an incomplete type");
                ^~~~~~~~~~~
unique_ptr.h:300:7: note: in instantiation of member function 'std::default_delete<Point::Impl>::operator()' requested here
    __ptr_.second()(__tmp);
    ^
unique_ptr.h:266:75: note: in instantiation of member function 'std::unique_ptr<Point::Impl>::reset' requested here
  _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR_SINCE_CXX23 ~unique_ptr() { reset(); }

Looks like the compiler is trying to do something on an incomplete type, which doesn’t work out. What’s actually missing here is a destructor required for std::unique_ptr doing its job.

Fine, let’s add a destructor to Point:

// Point.h

class Point
{
  //...
  ~Point() = default; // error
  //...
}

Too bad, still doesn’t compile, but the compiler hints are a little more useful now:

Point.h:34:5: note: in instantiation of member function
'std::unique_ptr<Point::Impl>::~unique_ptr' requested here
  ~Point() = default;  // error

What’s going on? In order to use a std::unique_ptr class member, we need a destructor being available. However, the defaulted destructor in the header is not sufficient because Impl is still an incomplete type. What’s actually needed is:

  1. A destructor declared in the header file.
  2. A destructor defined in the implementation in file.

The destructor declaration becomes

// Point.h
class Point
{
  // ...
  ~Point();
  // ...
}

and in the implementation file we add

// Point.cpp
Point::~Point() = default;

Putting this all together, we finally have a working implementation of the opaque pointer pattern using std::unique_ptr. Yay!

To round this up, let’s briefly review some pros and cons.

Advantages

The main advantage obviously is better information hiding: Private implementation details are not exposed to the users of a class. Even more, consistent usage of opaque pointers can be used to ensure binary compatibility, although I didn’t try that in practice, yet.

Another advantage are reduced compilation dependencies. If you ever worked with a large C++ code base you probably know how annoying long compile times can get. The more definitions you have in your headers the more dependencies you have and the more compilation units need to be re-compiled upon a change. Consistent use of opaque pointers and forward declarations can help to mitigate this problem.

Drawbacks

Everything has a cost, and this pattern is no exception. There are two main points to consider:

  1. Implementation overhead
  2. Run-time overhead

As for (1), consistently using this pattern can be a chore and requires engineering discipline in your team. Code reviews and checklists help.

As for (2), there is a certain overhead involved: at least one level of indirection de-referencing the Impl pointer. This can be a no-go for performance-critical code. However, I’d consider the main application of opaque pointers to be in higher-level APIs that provides access to more complex and performance-critical functions of a deep module.

Wrapping up

After reading this article, you should understand the basics of the opaque pointer pattern and how you can implement it using std::unique_ptr. I also gave some hints on when it is appropriate to use it and when maybe not.

Admittedly, this article is only scratching the surface of the subject. There’s more to say about opaque pointers, notably when it comes to moving and copying things around. I’ll save that for another day.

Further Reading

  1. Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1995)