C++: Pimpl

Let’s say we defined this class inside a Reader.DLL DLL:

class DLLEXPORT Reader
{
  public:
    Reader(const std::string& filename);
    ~Reader();        

    std::string ReadLine() const;
    bool        IsEndOfFile()    const;        

  private:
    FILE* file;
};

This class lets us read from a file once.

What if later we want to use this same class to read from a file several times? We can modify it to:

class DLLEXPORT Reader
{
  public:
    Reader(const std::string& filename);
    ~Reader();
    
    std::string ReadLine() const;
    bool IsEndOfFile() const;
    void Restart();

  private:
    std::string filename;
    FILE* file;
};

What I have done is adding the file name as a class attribute to reopen the file when we will invoke to “Restart” using the file name.

Now let’s imagine we have a function that uses the first version of our Reader.DLL:

void ShowFile(const std::string& file)
{
  Reader reader(file);
  while (!reader.IsEndOffile())
  {
    cout << reader.readLine() << endl;
  }
} 

The problem appears when the user wants to link his code using the second version of our Reader.DLL. The program will not work properly or will crash or will not work at all. Why?

Because though the API of the second version is compatible with the API of our first version (the code will link perfectly), the ABIs are not equal. The ABI is the “Application Binary Interface” and says us how the binaries should get linked… Why our ABI is not compatible? Because I have added a “filename” attribute in the same place where the “file” attribute was located before, so, every reference to “file” inside our program, will refer to “filename”, and since both things have different types, the program will get crazy.

This problem occurs because in our class header we are declaring explicitly the class attributes (a C++ well-known problem about encapsulation). This problem also can occur if, let’s say, we do not add or remove any method in the class, but we replace the private attributes for other ones (for example, changing the FILE* to fstream).

The “pimpl idiom” or “opaque pointer” or “cheshire cat”, is a way to avoid this problem in C++. The idea behind this idiom is provide in the class interface (.h) a pointer to a struct which will store the class attributes; but such struct will be defined inside the .cpp. Doing this, several problems get fixed:

ABI compatibility is provided because the attributes used in the class implementation are not published in the .h, so, they are not being published in the DLL (they get used internally only).
A high level of encapsulation is provided (the .h files will be delivered just with the stuff the user needs to know).

If the implementation has changed, but not the interface, recompiling the project that uses our .h will not be needed (because the .h got unmodified).

So, how my example will look?

VERSION 1: Interface: “Reader.h”

class ReaderData; //"forward declaration"

class DLLEXPORT Reader
{
  public:
    Reader(const std::string& filename);
    ~Reader();

    std::string ReadLine() const;
    bool IsEndOfFile() const;

  private:
    ReaderData* pData; //pointer to the class attrs
}; 

Implementation: “Reader.cpp”

#include "Reader.h"

//Here we define the struct to use
struct ReaderData
{
  FILE* file;
};

Reader::Reader(const std::string& n)
{
  pData = new ReaderData();
  pData->file = fopen(n.c_str(), "r");
}

Reader::~Reader()
{
  fclose(pData->file);
  delete pData;
}

std::string Reader::ReadLine() const
{
  char aux[256];
  fgets(aux, 256, pData->file);
  return std::string(aux);
}

bool Reader::IsEndOfFile() const
{
  return feof(pData->file);
} 

VERSION 2: Interface: “Reader.h”

class ReaderData; //"forward declaration"

class DLLEXPORT Reader
{
  public:
    Reader(const std::string& filename);
    ~Reader();

    std::string ReadLine() const;
    bool IsEndOfFile() const;
    void Restart();

  private:
    ReaderData* pData;
}; 

Implementation: “Reader.cpp”

#include "Reader.h"

struct ReaderData
{
  std::string filename; //new attribute for version 2
  FILE* file;
};

Reader::Reader(const std::string& n)
{
  pData = new ReaderData();
  pData->filename = n;
  pData->file = fopen(n.c_str(), "r");
}

Reader::~Reader()
{
  fclose(pData->file);
  delete pData;
}

std::string Reader::ReadLine() const
{
  char aux[256];
  fgets(aux, 256, pData->file);
  return std::string(aux);
}

bool Reader::IsEndOfFile() const
{
  return feof(pData->file);
}

void Reader::Restart()
{
  fclose(pData->file);
  pData->file = fopen(pData->filename.c_str(), "r");
} 

If the Reader.dll programmer implemented his stuff using the “pimpl idiom” since his first version, his new Reader.dll will not affect at all to the Reader.dll consumers because the new one is API and ABI backwards compatible.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s