How does the pimpl idiom reduce dependencies?

前端 未结 7 2065
我在风中等你
我在风中等你 2020-12-10 05:35

Consider the following:

PImpl.hpp

class Impl;

class PImpl
{
    Impl* pimpl;
    PImpl() : pimpl(new Impl) { }
    ~PImpl() { delete pimpl; }
    vo         


        
7条回答
  •  误落风尘
    2020-12-10 06:28

    There has been a number of answers... but no correct implementation so far. I am somewhat saddened that examples are incorrect since people are likely to use them...

    The "Pimpl" idiom is short for "Pointer to Implementation" and is also referred to as "Compilation Firewall". And now, let's dive in.

    1. When is an include necessary ?

    When you use a class, you need its full definition only if:

    • you need its size (attribute of your class)
    • you need to access one of its method

    If you only reference it or have a pointer to it, then since the size of a reference or pointer does not depend on the type referenced / pointed to you need only declare the identifier (forward declaration).

    Example:

    #include "a.h"
    #include "b.h"
    #include "c.h"
    #include "d.h"
    #include "e.h"
    #include "f.h"
    
    struct Foo
    {
      Foo();
    
      A a;
      B* b;
      C& c;
      static D d;
      friend class E;
      void bar(F f);
    };
    

    In the above example, which includes are "convenience" includes and could be removed without affecting the correctness ? Most surprisingly: all but "a.h".

    2. Implementing Pimpl

    Therefore, the idea of Pimpl is to use a pointer to the implementation class, so as not to need to include any header:

    • thus isolating the client from the dependencies
    • thus preventing compilation ripple effect

    An additional benefit: the ABI of the library is preserved.

    For ease of use, the Pimpl idiom can be used with a "smart pointer" management style:

    // From Ben Voigt's remark
    // information at:
    // http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Checked_delete
    template 
    inline void checked_delete(T * x)
    {
        typedef char type_must_be_complete[ sizeof(T)? 1: -1 ];
        (void) sizeof(type_must_be_complete);
        delete x;
    }
    
    
    template 
    class pimpl
    {
    public:
      pimpl(): m(new T()) {}
      pimpl(T* t): m(t) { assert(t && "Null Pointer Unauthorized"); }
    
      pimpl(pimpl const& rhs): m(new T(*rhs.m)) {}
    
      pimpl& operator=(pimpl const& rhs)
      {
        std::auto_ptr tmp(new T(*rhs.m)); // copy may throw: Strong Guarantee
        checked_delete(m);
        m = tmp.release();
        return *this;
      }
    
      ~pimpl() { checked_delete(m); }
    
      void swap(pimpl& rhs) { std::swap(m, rhs.m); }
    
      T* operator->() { return m; }
      T const* operator->() const { return m; }
    
      T& operator*() { return *m; }
      T const& operator*() const { return *m; }
    
      T* get() { return m; }
      T const* get() const { return m; }
    
    private:
      T* m;
    };
    
    template  class pimpl {};
    template  class pimpl {};
    
    template 
    void swap(pimpl& lhs, pimpl& rhs) { lhs.swap(rhs); }
    

    What does it have that the others didn't ?

    • It simply obeys the Rule of Three: defining the Copy Constructor, Copy Assignment Operator and Destructor.
    • It does so implementing the Strong Guarantee: if the copy throws during an assignment, then the object is left unchanged. Note that the destructor of T should not throw... but then, that is a very common requirement ;)

    Building on this, we can now define Pimpl'ed classes somewhat easily:

    class Foo
    {
    public:
    
    private:
      struct Impl;
      pimpl mImpl;
    }; // class Foo
    

    Note: the compiler cannot generate a correct constructor, copy assignment operator or destructor here, because doing so would require access to Impl definition. Therefore, despite the pimpl helper, you will need to define manually those 4. However, thanks to the pimpl helper the compilation will fail, instead of dragging you into the land of undefined behavior.

    3. Going Further

    It should be noted that the presence of virtual functions is often seen as an implementation detail, one of the advantages of Pimpl is that we have the correct framework in place to leverage the power of the Strategy Pattern.

    Doing so requires that the "copy" of pimpl be changed:

    // pimpl.h
    template 
    pimpl::pimpl(pimpl const& rhs): m(rhs.m->clone()) {}
    
    template 
    pimpl& pimpl::operator=(pimpl const& rhs)
    {
      std::auto_ptr tmp(rhs.m->clone()); // copy may throw: Strong Guarantee
      checked_delete(m);
      m = tmp.release();
      return *this;
    }
    

    And then we can define our Foo like so

    // foo.h
    #include "pimpl.h"
    
    namespace detail { class FooBase; }
    
    class Foo
    {
    public:
      enum Mode {
        Easy,
        Normal,
        Hard,
        God
      };
    
      Foo(Mode mode);
    
      // Others
    
    private:
      pimpl mImpl;
    };
    
    // Foo.cpp
    #include "foo.h"
    
    #include "detail/fooEasy.h"
    #include "detail/fooNormal.h"
    #include "detail/fooHard.h"
    #include "detail/fooGod.h"
    
    Foo::Foo(Mode m): mImpl(FooFactory::Get(m)) {}
    

    Note that the ABI of Foo is completely unconcerned by the various changes that may occur:

    • there is no virtual method in Foo
    • the size of mImpl is that of a simple pointer, whatever what it points to

    Therefore your client need not worry about a particular patch that would add either a method or an attribute and you need not worry about the memory layout etc... it just naturally works.

提交回复
热议问题