Search code examples
c++design-patternsabstraction

Handle Body Idiom in C++


I have a class in my library which I want to expose to the users. I don't want to expose the whole class as I might want to make a binary incompatible changes later. I am confused with which of the following ways would be best.

Case 1:

struct Impl1;
struct Handle1
{
  // The definition will not be inline and will be defined in a C file
  // Showing here for simplicity
  void interface()
  {
    static_cast<Impl1*>(this)->interface();
  }
}

struct Impl1 : public Handle1
{
  void interface(){ /* Do ***actual*** work */ }
  private:
  int _data; // And other private data
};

Case 2:

struct Impl2
struct Handle2
{
  // Constructor/destructor to manage impl
  void interface() // Will not be inline as above.
  {
    _impl->interface();
  }
  private:
  Impl2* _impl;
}

struct Impl2
{
  void interface(){ /* Do ***actual*** work */ }
  private:
  int _data; // And other private data
};

The Handle class is only for exposing functionality. They will be created and managed only inside the library. Inheritance is just for abstracting implementation details. There won't be multiple/different impl classes. In terms of performance, I think both will be identical. Is it? I am thinking of going with the Case 1 approach. Are there any issues that needs to be taken care of?


Solution

  • Your second approach looks very much like the compilation firewall idiom (sometimes known as the PIMPL idiom).

    The only difference is that in the compilation firewall idiom, the implementation class is usually (but not always) defined as a member. Don't forget the constructor (which allocates the Impl) and the destructor (which frees it). Along with the copy constructor and assignment operator.

    The first approach also works, but it will require factory functions to create the objects. When I've used it, I've simply made all of the functions in the Handle pure virtual, and let the client code call them directly.

    In this case, since client code actually has pointers to your object (in the compilation firewall idiom, the only pointers are in the Handle class itself), and the client will have to worry about memory management; if no cycles are possible, this is one case where shared_ptr makes a lot of sense. (The factory function can return a shared_ptr, for example, and client code may never see a raw pointer.)