pimpl成语如何减少依赖?

Bil*_*eal 8 c++ pimpl-idiom

考虑以下:

PImpl.hpp

class Impl;

class PImpl
{
    Impl* pimpl;
    PImpl() : pimpl(new Impl) { }
    ~PImpl() { delete pimpl; }
    void DoSomething();
};
Run Code Online (Sandbox Code Playgroud)

PImpl.cpp

#include "PImpl.hpp"
#include "Impl.hpp"

void PImpl::DoSomething() { pimpl->DoSomething(); }
Run Code Online (Sandbox Code Playgroud)

Impl.hpp

class Impl
{
    int data;
public:
    void DoSomething() {}
}
Run Code Online (Sandbox Code Playgroud)

client.cpp

#include "Pimpl.hpp"

int main()
{
    PImpl unitUnderTest;
    unitUnderTest.DoSomething();
}
Run Code Online (Sandbox Code Playgroud)

这种模式背后的想法是,Impl界面可以改变,但客户端不必重新编译.然而,我没有看到这是如何真实的情况.假设我想在这个类中添加一个方法 - 客户端仍然需要重新编译.

基本上,只有种这样的,我可以看到的变化不断需要改变头文件一类的东西,其中的类发生变化的接口.当发生这种情况时,pimpl或没有pimpl,客户端必须重新编译.

这里的哪种编辑在不重新编译客户端代码方面给我们带来了好处?

Ala*_*lan 10

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

此外,您不一定会将您的impl类放在自己的标头中.只需将它作为单个cpp中的结构,并使外部类直接引用其数据成员.

编辑: 示例

SomeClass.h

struct SomeClassImpl;

class SomeClass {
    SomeClassImpl * pImpl;
public:
    SomeClass();
    ~SomeClass();
    int DoSomething();
};
Run Code Online (Sandbox Code Playgroud)

SomeClass.cpp

#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();
}
Run Code Online (Sandbox Code Playgroud)

  • 您的示例调用未定义的行为:您忘记了"臭名昭着"的三个规则>>每当您定义复制构造函数,复制赋值运算符或析构函数之一时,请定义另外两个. (2认同)
  • 我不同意,不幸的是,许多初学者只会复制/粘贴并调整你的例子,因此他们最终会手上的疼痛.此外,由于异常处理(取决于您选择的实现),它们可能不是微不足道的.@Ben:双倍自由是我对UB的意思. (2认同)

Mat*_* M. 6

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

"Pimpl"成语是"指向实现的指针"的缩写,也称为"编译防火墙".现在,让我们潜入.

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);
};
Run Code Online (Sandbox Code Playgroud)

在上面的例子中,包括"方便"包括并且可以删除而不影响正确性?最令人惊讶的是:除了"啊"之外.

2.实施Pimpl

因此,Pimpl的想法是使用指向实现类的指针,以便不需要包含任何头:

  • 从而将客户端与依赖关系隔离开来
  • 从而防止编译涟漪效应

另一个好处是:保留了库的ABI.

为了便于使用,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); }
Run Code Online (Sandbox Code Playgroud)

其他人没有的是什么?

  • 它只遵循规则三:定义复制构造函数,复制赋值运算符和析构函数.
  • 它实现了强保证:如果在赋值期间抛出副本,则对象保持不变.请注意,析构函数不T应该抛出......但是,这是一个非常常见的要求;)

在此基础上,我们现在可以轻松地定义Pimpl的类:

class Foo
{
public:

private:
  struct Impl;
  pimpl<Impl> mImpl;
}; // class Foo
Run Code Online (Sandbox Code Playgroud)

注意:编译器无法在此生成正确的构造函数,复制赋值运算符或析构函数,因为这样做需要访问Impl定义.因此,尽管pimpl帮手,您将需要手动不过来定义那些4,多亏了平普尔助手编译将会失败,而不是你拖到未定义行为的土地.

3.走得更远

应该注意的是,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;
}
Run Code Online (Sandbox Code Playgroud)

然后我们就可以定义我们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)) {}
Run Code Online (Sandbox Code Playgroud)

请注意,ABI Foo完全不受可能发生的各种变化的影响:

  • 没有虚拟方法 Foo
  • 大小mImpl是一个简单的指针,无论它指向什么

因此,您的客户不必担心会添加方法或属性的特定补丁,您不必担心内存布局等......它只是自然有效.


Chu*_*dad 5

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

BTW,在所示的代码中,IMPL和PIMPL之间存在强耦合.因此,IMPL的类实现中的任何更改也会导致需要重建.

  • 实现包括数据成员,如`data`和私有方法.这些会改变`Impl.h`(如果它存在)但不改变`PImpl.h`. (2认同)