Search code examples
c++pimpl-idiom

Constructor and destructor in c++ when using the pimpl idiom


I come from Java that has a different way in handling what's private and has to be hided regarding a class implementation and it also has a garbage collector which means there is no need for a destructor.

I learned the basics of how to implement a class in c++ but I need to better understand how to implement a class, in particular the constructor and destructor, when using the pimpl idiom.

hpp file:

class MyClass{
public:
  MyClass();
  MyClass(std::vector<int>& arr);
  ~MyClass();

 
private:
  struct Impl;
  Impl* pimpl;
};

cpp file:

#include "MyClass.hpp"
using namespace std;


struct MyClass::Impl{
  vector<int> arr;
  int var;
};

I wrote the sample code of a class I need to work on(which means I can't change the way the pimpl idiom is used) using the pimpl idiom and I'm looking for an answer to these questions:

  • How do I implement the constructor to create an empty instance of MyClass?
MyClass::MyClass(){}
  • How do I implement the constructor to create an instance of MyClass with these arguments?
MyClass::MyClass(vector<int>& arr,int var){}
  • How do I implement the destructor?
MyClass::~MyClass(){}

EDIT:

How I would do it:

MyClass::MyClass(): pimpl(new Impl) {}

MyClass::MyClass(vector<int>& arr,int var): pimpl(new Impl) {
  pimpl->arr=arr;
  pimpl->var=var;
}

MyClass::~MyClass(){
  delete pimpl;
}


Solution

  • This is an example of a correct way to implement PIMPL idiom in modern C++:

    foo.hpp

    #pragma once
    
    #include <memory>
    
    class Foo {
    public:
      Foo();
      ~Foo();
    
      Foo(Foo const &) = delete;
      Foo &operator=(Foo const &) = delete;
    
      Foo(Foo &&) noexcept;
      Foo &operator=(Foo &&) noexcept;
    
      void bar();
    
    private:
      class impl;
      std::unique_ptr<impl> pimpl_;
    };
    

    foo.cpp:

    #include "foo.hpp"
    
    #include <iostream>
    
    class Foo::impl {
    public:
      impl() = default;
      ~impl() = default;
    
      impl(impl const &) = default;
      impl &operator=(impl const &) = default;
    
      impl(impl &&) noexcept = default;
      impl &operator=(impl &&) noexcept = default;
    
      void bar() { std::cout << "bar" << std::endl; }
    };
    
    Foo::Foo() : pimpl_(new impl{}) {}
    Foo::~Foo() = default;
    
    Foo::Foo(Foo &&) noexcept = default;
    Foo &Foo::operator=(Foo &&) noexcept = default;
    
    void Foo::bar() { pimpl_->bar(); }
    

    main.cpp:

    #include "foo.hpp"
    
    int main(int argc, char const *argv[]) {
      Foo foo;
      foo.bar();
      return 0;
    }
    

    There are a few words that are to be said:

    • use a smart pointer (unique or shared) to hold reference(s) to 'impl' object. It'll help you like in JAVA control number of references to an underlying object. In this example, as just Foo class is gone 'impl' is automatically destroyed, there is no need to manually watch the lifetime of the 'impl' object
    • 'impl' MUST be always completely declared and defined in *.cpp file (or bunch of files) or internal *.hpp and/or *.cpp files so that a user of your front class (i.e., in this example 'Foo') that aggregates 'impl' is impossible to see any changes and content of your 'impl' class, that is why PIMPL idiom exists: to persist the interface of the front class for a user and all the changes mostly shall be done in the hidden 'impl' class
    • The destructor of the front class MUST be always defined in *.cpp file since the compiler must know how to destroy the 'impl' that is not seen thoroughly from the *.hpp file where a front class is declared
    • The special functions with rvalue-references (move-ctor and move-assignment operator) MUST also be defined in *.cpp file due to the same reason as in the previous clause