pimpl成语如何减少依赖?

时间:2010-08-30 03:33:09

标签: c++ pimpl-idiom

请考虑以下事项:

PImpl.hpp

class Impl;

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

PImpl.cpp

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

void PImpl::DoSomething() { pimpl->DoSomething(); }

Impl.hpp

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

client.cpp

#include "Pimpl.hpp"

int main()
{
    PImpl unitUnderTest;
    unitUnderTest.DoSomething();
}

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

基本上,我可以看到曾经需要更改类的头文件的唯一类型的更改是类的接口更改的内容。当发生这种情况时,pimpl或没有pimpl,客户端必须重新编译。

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

7 个答案:

答案 0 :(得分:10)

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

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

编辑示例

SomeClass.h

struct SomeClassImpl;

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

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

答案 1 :(得分: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);
};

在上面的例子中,包括“方便”包括并且可以删除而不影响正确性?最令人惊讶的是:除了“a.h”之外的其他所有内容。

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

其他人没有的是什么?

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

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

class Foo
{
public:

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

注意:编译器无法在此生成正确的构造函数,复制赋值运算符或析构函数,因为这样做需要访问Impl定义。因此,尽管有pimpl帮助器,您还需要手动定义那些4.但是,由于pimpl帮助器,编译将失败,而不是将您拖入未定义行为的土地。

第3。更进一步

应该注意的是,virtual函数的存在通常被视为一个实现细节,Pimpl的一个优点是我们有适当的框架来利用策略模式的力量。 / p>

这样做需要更改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)) {}

请注意Foo的ABI完全不关心可能发生的各种变化:

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

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

答案 2 :(得分:5)

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

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

答案 3 :(得分:4)

考虑一些更现实的东西,其好处变得更加显着。大部分时间我都使用它来进行编译器防火墙和实现隐藏,我在可见类所在的同一编译单元中定义了实现类。在你的例子中,我不会有Impl.h或{{1 }和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中有许多未公开的其他方法。这也使得这种技术适用于分层实现。

答案 4 :(得分:3)

在您的示例中,您可以更改data的实现,而无需重新编译客户端。没有PImpl中介就不会出现这种情况。同样,您可以更改Imlp::DoSomething的签名或名称(到某一点),客户也不必知道。

通常,可以在不重新编译客户端的情况下更改private中可以声明的任何内容(默认值)或protected {/ 1}}。

答案 5 :(得分:1)

non-Pimpl 类标题中,.hpp文件将所有类的公共和私有组件定义在一个大桶中。

私有与您的实现密切相关,这意味着您的.hpp文件确实可以为您的内部实现提供很多帮助。

考虑选择在类中私有使用的线程库之类的东西。不使用Pimpl,线程类和类型可能会作为私有成员或私有方法上的参数遇到。好的,一个线程库可能是一个不好的例子,但是你明白了:你的类定义的私有部分应该远离那些包含你的标题的人。

这就是Pimpl的用武之地。由于公共类标题不再定义“私有部分”,而是有一个指向实现的指针,你的私有世界仍然隐藏在“#include”s的逻辑中你的公共类头。

当您更改私有方法(实现)时,您正在更改隐藏在Pimpl下面的内容,因此您的类的客户端不需要重新编译,因为从他们的角度来看,没有任何改变:他们不再看到私人实施成员。

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

答案 6 :(得分:1)

并非所有类都受益于p-impl。您的示例在其内部状态中只有原始类型,这解释了为什么没有明显的好处。

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