Search code examples
c++pimpl-idiom

Can you use the pimpl idiom for use across multiple cpp files with one class?


For instance, here is what I mean. Let's say you have a single header file with a single pimpl class. Can you define the functions of this class across two cpp files without redefining the variables of the class?

I've tried this before using a static variable for the pointer and a redefinition in both files. I keep running into issues regarding class variables being erased when moving across files, however.

//Header
class PRIVATE {
  struct Test2;

public:
  struct Test;

  std::shared_ptr<Test> Client_ptr;

  PRIVATE();

}; //PRIVATE

static std::shared_ptr<PRIVATE> PB = std::shared_ptr<PRIVATE>();
//Cpp1
//Implementation for Private
//Implementation for Test1

//Function not inside either class, references PB, defined in Cpp2  -> READ ACCESS VIOLATION
//Cpp2
//Definition Goes Here
//Implementation for Test2

//Function not inside either class, references PB, defined in Cpp1  -> READ ACCESS VIOLATION

Solution

  • One issue with splitting pimpl internals across separate compilation units is that you need at least one compilation unit that knows how to destroy all members.

    For example:

    //main.h
    class Main {
      struct Test;
      struct Test2;
      std::unique_ptr<Test> pimpl1;
      std::unique_ptr<Test2> pimpl2;
    public:
      Main();
      ~Main();
    };
    
    //test1.cpp
    struct Main::Test {
    };
    // we don't know what Test2 is, so we cannot define Main::~Main().
    
    //test2.cpp
    struct Main::Test2 {
    };
    // we don't know what Test is, so we cannot define Main::~Main().
    

    One solution is this:

    class Main {
      struct Test;
      struct Test2;
      std::unique_ptr<Test> pimpl1;
      struct Defer {
        std::unique_ptr<Test2> pimpl2;
        Defer();
        ~Defer();
      };
      Defer defer;
    public:
      Main();
      ~Main();
    };
    

    Now, Main::Defer::~Defer() can live in test2.cpp where it knows how to destroy its pimpl2 instance but doesn't need to know how to destroy pimpl. Similarly, since Main::Defer is defined (not just declared) in main.h, Main::~Main() can properly destroy its defer member:

    //test1.cpp
    /* Test definition */
    Main::Main() : pimpl(std::make_unique<Test>()), defer() {}
    Main::~Main() {}
    
    //test2.cpp
    /* Test2 definition */
    Main::Defer::Defer() : pimpl2(std::make_unique<Test2>()) {}
    Main::Defer::~Defer() {}
    

    It's still difficult to have Test and Test2 talk between each other but that is kind of the point of pimpl. It can be facilitated by Main offering some interfaces or by some intermediate header that declares some interfaces.