Updated: Aug 15, 2018
In this SSP (Stupidly-Simple-Patterns) series, I will highlight various simple but useful design patterns in C++. They worked in C++98, they still work in C++17! These patterns are more tools in your kit of design tools. After decades of use in various shapes and forms, they are proven to be reusable, making code flexible, robust and extensible in long run. The topic of discussion for this article is – Bridge pattern (a.k.a handle-body or Pimpl Idiom).
Have you ever written a tool or utility or piece of software which was supposed to run on more than one platform, or had to support multiple implementations? Even if not, it’s easy to bump up with the requirement to support a piece of code which is supposed to do that, for example, while prototyping, you may want to experiment with various implementations which trade-off between performance and resource requirements. Bridge pattern comes handy exactly for such situations. You only realize its power once you use it and it gets into play.
Let's take an example, Let’s say we want to define some task class which could be used inside thread pool (most open source libraries, like boost, for example, will provide base class from which your Task class would inherit). Now this Task could be run with the same thread pool but could be implemented in multiple ways. We will create Task.h and Task.cpp like this:
Take moment, take a deep breath and go through that code again. You will see that Task definition in the header file (Task.h) reveals nothing about implementation, while Task.cpp has all glory details of implantation. Imagine if this was class you were writing for the common code or a library, your clients would be happier since they will include Task.h and won’t even need to recompile (but just relink) whenever you change Task implementation to do something differently (all those changes would be in Task.cpp which won’t be seen by your library or common code client). Isn’t that nice? It absolutely is! This is so commonly used application of bridge pattern that it has its own name – Pimpl Idiom (pointer-to-implementation).
Proper usage of “Pimpl” would reduce build times for your project drastically! This compile time is especially an important aspect if your library or common code is either heavily used or is frequently updated during the maintenance period of the software.
You might also be wondering why UniqueTask destructor is declared in Task.h, and then there is a fancy looking statement like this in Task.cpp:
What above code tells compiler is that destructor is declared and defined somewhere else. This is important when client code includes your .h file and when someone writes a statement like this:
At this stage, the compiler will think UniqueTask is incomplete object type as it needs to find the code for its destruction but it hasn’t come across it. The compiler could very well (and most times it will) try to write a default definition of a destructor for you but in this case, it can’t, as doing that will mean it also needs to write destruction code for forward declared UniqueTaskImpl class. Which is why we declare it to satisfy compiler for places which use this Task.h (client code). But declaring a destructor would mean we define it in Task.cpp and we really don’t have much to add to a destructor code to what compiler default would generate, so which is why we ask the compiler to generate a definition for us. That is exactly what “=default” syntax in CPP file is doing. Note though, this default syntax will only work from C++11 onwards.
Also notice the smart usage of smart pointer in the example above! Std::unique_ptr is part of the standard library now from C++11 onwards. Use it wherever you want exclusive ownership of the pointers your code is dealing with. You will soon realize pointer leaks is thing of the past by employing right smart pointers for the job!
Okay, so you might be thinking by now that all this is well and good but how does this help you switch implementations dynamically? Let’s take now another example interface and build that use case on top of Pimpl idiom we saw above. Let’s say we have a class which does some processing and its processing is implemented in Pimpl class, not that class itself then needs to have a polymorphic use, like this:
Take a minute again to digest this code. If you observe carefully, you see we now have class implementation delegated to its own class and that class is a pure virtual interface or abstract class. This means client code can derive from that Implementation interface class and supply unique implementation according to its need, and all that happens while instantiating abstract class (AbstractMyClass) instantiation. And as stated in example use case, we could change guts of our class (MyClient) on-the-fly with performance-optimized or storage-optimized implementations.
Typically, the implementation need not always be chosen runtime, they could also be typedef’d compile time to choose, or they could still be chosen runtime based on an environment variable or some kind of user input, like a config file, for example. That is what makes bridge pattern very powerful.
A Bridge is a very powerful pattern. It allows you to change implementation or guts of your class by switching between different implementation classes on-the-fly or based on some criteria. This allows the same class to have different implementations. This is also a useful tool for prototyping for big projects and play around with different implementation, or for cases where you really need different implementations in the same piece of code (e.g. to run the same utility on different platforms). So the bridge pattern essentially decouples interface from its implementation and allows one to choose between implementations.