代码之家  ›  专栏  ›  技术社区  ›  Billy ONeal IS4

pimpl习惯用法如何减少依赖关系?

  •  8
  • Billy ONeal IS4  · 技术社区  · 15 年前

    考虑以下几点:

    水电站

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

    #include "PImpl.hpp"
    #include "Impl.hpp"
    
    void PImpl::DoSomething() { pimpl->DoSomething(); }
    

    Impl.hpp公司

    class Impl
    {
        int data;
    public:
        void DoSomething() {}
    }
    

    客户端.cpp

    #include "Pimpl.hpp"
    
    int main()
    {
        PImpl unitUnderTest;
        unitUnderTest.DoSomething();
    }
    

    这种模式背后的理念是 Impl 仍然 必须重新编译。

    基本上,我能看到的只有这样的变化 曾经 需要更改的类的头文件是类的接口更改的对象。当这种情况发生时,无论是否有pimpl,客户机都必须重新编译。

    7 回复  |  直到 15 年前
        1
  •  10
  •   Alan    15 年前

    主要的优点是接口的客户机不必包含所有类的内部依赖项的头。因此,对这些标题的任何更改都不会级联到大多数项目的重新编译中。再加上关于实现隐藏的一般理想主义。

    另外,您不必将impl类放在它自己的头中。只需使它成为单个cpp中的结构,并使外部类直接引用其数据成员。

    编辑: 例子

    struct SomeClassImpl;
    
    class SomeClass {
        SomeClassImpl * pImpl;
    public:
        SomeClass();
        ~SomeClass();
        int DoSomething();
    };
    

    #include "SomeClass.h"
    #include "OtherClass.h"
    #include <vector>
    
    struct SomeClassImpl {
        int foo;
        std::vector<OtherClass> otherClassVec;   //users of SomeClass don't need to know anything about OtherClass, or include its header.
    };
    
    SomeClass::SomeClass() { pImpl = new SomeClassImpl; }
    SomeClass::~SomeClass() { delete pImpl; }
    
    int SomeClass::DoSomething() {
        pImpl->otherClassVec.push_back(0);
        return pImpl->otherClassVec.size();
    }
    
        2
  •  7
  •   Matthieu M.    15 年前

    有很多答案。。。但到目前为止还没有正确的实施。我有点难过的例子是不正确的,因为人们可能会使用它们。。。

    1什么时候需要包含?

    使用类时,仅当满足以下条件时才需要其完整定义:

    • 您需要它的大小(类的属性)
    • 你需要访问它的一个方法

    如果只引用它或有指向它的指针,那么由于引用或指针的大小不依赖于引用/指向的类型,因此只需要声明标识符(正向声明)。

    #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);
    };
    

    2实施Pimpl

    因此,Pimpl的思想是使用指向实现类的指针,这样就不需要包含任何头文件:

    • 从而将客户机与依赖项隔离开来
    • 从而防止了编译的连锁反应

    为了便于使用,Pimpl习惯用法可以与“智能指针”管理样式一起使用:

    // From Ben Voigt's remark
    // information at:
    // http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Checked_delete
    template<class T> 
    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 <typename T>
    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<T> 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 <typename T> class pimpl<T*> {};
    template <typename T> class pimpl<T&> {};
    
    template <typename T>
    void swap(pimpl<T>& lhs, pimpl<T>& rhs) { lhs.swap(rhs); }
    

    有什么其他人没有的?

    • 它只遵循三条规则:定义复制构造函数、复制赋值运算符和析构函数。
    • 强有力的保证 :如果副本在赋值期间抛出,则对象保持不变。注意,的析构函数 T

    在此基础上,我们现在可以很容易地定义Pimpl类:

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

    :编译器无法在此处生成正确的构造函数、复制赋值运算符或析构函数,因为这样做需要访问 Impl pimpl 助手,您将需要手动定义这4个。然而,多亏了pimpl助手,编译将失败,而不是将您拖入未定义行为的领域。

    virtual 函数通常被视为一个实现细节,Pimpl的优点之一是我们有适当的框架来利用策略模式的力量。

    这样做需要更改pimpl的“副本”:

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

    然后我们就可以定义 Foo 就像这样

    // foo.h
    #include "pimpl.h"
    
    namespace detail { class FooBase; }
    
    class Foo
    {
    public:
      enum Mode {
        Easy,
        Normal,
        Hard,
        God
      };
    
      Foo(Mode mode);
    
      // Others
    
    private:
      pimpl<detail::FooBase> 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)) {}
    

    请注意

    • 中没有虚拟方法
    • mImpl 是一个简单的指针,不管它指向什么

    因此,您的客户不必担心会添加方法或属性的特定修补程序,您也不必担心内存布局等。。。它只是自然而然地起作用。

        3
  •  5
  •   Chubsdad    15 年前

    使用PIMPL习惯用法,如果IMPL类的内部实现细节发生更改,则不必重建客户机。IMPL类(以及头文件)接口的任何更改显然都需要PIMPL类进行更改。

    顺便说一句, 在显示的代码中,IMPL和PIMPL之间存在强耦合。因此,IMPL类实现中的任何更改也会导致需要重建。

        4
  •  4
  •   D.Shawley    15 年前

    Impl.h Impl.cpp Pimpl.cpp 看起来像:

    #include <iostream>
    #include <boost/thread.hpp>
    
    class Impl {
    public:
      Impl(): data(0) {}
      void setData(int d) {
        boost::lock_guard l(lock);
        data = d;
      }
      int getData() {
        boost::lock_guard l(lock);
        return data;
      }
      void doSomething() {
        int d = getData();
        std::cout << getData() << std::endl;
      }
    private:
      int data;
      boost::mutex lock;
    };
    
    Pimpl::Pimpl(): pimpl(new Impl) {
    }
    
    void Pimpl::doSomething() {
      pimpl->doSomething();
    }
    

    现在没人需要知道我们对 boost . 当与政策结合在一起时,这会变得更加强大。线程策略之类的细节(例如,单线程与多线程)可以通过使用 Impl Impl公司 不会暴露的。这也使得这种技术有利于分层实现。

        5
  •  3
  •   Beta    15 年前

    data 无需重新编译客户端。没有PImpl中介,情况就不会如此。同样,您可以更改 Imlp::DoSomething

    一般来说,任何可以声明的 private protected 在里面 Impl

        6
  •  1
  •   Allbite    15 年前

    类头.hpp文件在一个大的bucket中定义类的公共和私有组件。

    private与您的实现紧密耦合,因此这意味着您的.hpp文件确实可以提供很多关于内部实现的信息。

    考虑一下类似 你选择在课堂内私下使用。如果不使用Pimpl,线程类和类型可能会作为私有成员或私有方法上的参数遇到。好吧,线程库可能是一个不好的例子,但是您得到了这样一个想法:类定义的私有部分应该隐藏起来,不让那些包含头的部分看到。

    指向实现的指针

    当您更改私有方法(实现)时,您正在更改隐藏在Pimpl下面的内容,因此您的类的客户机不需要重新编译,因为从他们的角度来看,什么都没有更改:它们不再是 看见

    http://www.gotw.ca/gotw/028.htm

        7
  •  1
  •   Ben Voigt    15 年前

    如果任何成员在另一个头中声明了复杂类型,那么可以看到p-impl将该头的包含从类的公共头移到实现文件中,因为您形成了指向不完整类型的原始指针(但不是嵌入字段或智能指针)。您可以单独使用指向所有成员变量的原始指针,但是使用指向所有状态的单个指针可以简化内存管理并改进数据的局部性(如果所有这些类型都依次使用p-impl,那么就没有太多的局部性)。

    推荐文章