BiDirectional Pimpl Pattern

BiDirectional Pimpl Pattern

While the Pimpl pattern is commonly used to hide implementation and private methods and variables from public interfaces, the common implementations often require a large amount of boilerplate code. This can make it hard to introduce this pattern into a large existing class. If we use bi-directional friend references with the private implementation, we can easily add a small private implementation into an existing class, allowing for better design patterns and performance optimizations to be gently introduced into existing code.

Consider the classic implementation:

// .h File
class DuckPond
{
   class Impl;
   std::unique_ptr<Impl> _impl;

public:
   void PreparePond();
   void LightsOn();
}

// Private Implementation
class DuckPond::Impl
{
private:
   void HeatPond();
   void LightsOn();

public:
   void PreparePond();
}

// .cpp File
void DuckPond::PreparePond()
{
   _impl->PreparePond();
}
void DuckPond::Impl::PreparePond()
{
   HeatPond();
   LightsOn();
}

The duplication involved in two PreparePond methods and moving all implementations and private member variables into the private implementation can impose a significant initial overhead when introducing this pattern to an existing class.

Instead, if we introduce references back and forth between the existing implementation and the private implementation, we can add a private implementation with minimal disruption.

// .h File
class DuckPond
{
   class Impl;
   std::unique_ptr<Impl> _impl;
   friend class Impl;

public:
   void PreparePond();
   void LightsOn();
}

// .cpp File
DuckPond::DuckPond() : _impl{ std::make_unique<Impl>(this) } {}

// Private Implementation
class DuckPond::Impl
{
   friend class DuckPond;
public:
   Impl(DuckPond* pubImpl);
    virtual ~Impl() = default;
private:
   DuckPond* _pubImpl;

public:
   void HeatPond();
}

In this example, we’re adding a heating system to the duck pond to thaw the winter ice, but we do not need to make any modifications to the lights, any private variables or have duplicate function definitions for the preparation and lighting routines.

void DuckPond::PreparePond()
{
   _impl->HeatPond();
   LightsOn();
}

void DuckPond::LightsOn() {}
void DuckPond::Impl::HeatPond() {}

While it breaks down some of the class encapsulation if we treat the combination of the public and private DuckPond classes together as a single entity, we have much simpler variable access across the public/private divide. This allows small routines and functionality to be slowly integrated into the interface to add new functionality, improve compile times, or restructure the class incrementally.

void DuckPond::Impl::HeatPond()
{
   if(getSurfaceCondition() == SurfaceState::IceFree)
   {
      _pubImpl->LightsOn();
      startDuckFeeder();
   }
}

When the private implementation methods then have access to both their private state, we can quickly and easily add a pimpl pattern to gain its benefits. It only involves changing the small amount of code we consider important for the current changeset to gain some of the benefits of the pimpl pattern without needing any major changes. This increases the likelihood the pattern will be adopted and provides a stable upgrade path to introduce new methods into existing code.

Futher Reading:

Leave a Reply

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