pimpl 惯用法如何减少依赖性?

发布于 2024-09-16 04:39:33 字数 809 浏览 7 评论 0原文

请考虑以下内容:

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,客户端都必须重新编译。

这里什么样的编辑可以给我们带来无需重新编译客户端代码的好处?

Consider the following:

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

The idea behind this pattern is that Impl's interface can change, yet clients do not have to be recompiled. Yet, I fail to see how this can truly be the case. Let's say I wanted to add a method to this class -- clients would still have to recompile.

Basically, the only kinds of changes like this that I can see ever needing to change the header file for a class for are things for which the interface of the class changes. And when that happens, pimpl or no pimpl, clients have to recompile.

What kinds of editing here give us benefits in terms of not recompiling client code?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

白日梦 2024-09-23 04:39:33

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

另外,您不必将 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();
}

The main advantage is that the clients of the interface aren't forced to include the headers for all your class's internal dependencies. So any changes to those headers don't cascade into a recompile of most of your project. Plus general idealism about implementation-hiding.

Also, you wouldn't necessarily put your impl class in its own header. Just make it a struct inside the single cpp and make your outer class reference its data members directly.

Edit: Example

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();
}
↘人皮目录ツ 2024-09-23 04:39:33

已经有很多答案......但到目前为止还没有正确的实施。我对示例不正确感到有些遗憾,因为人们可能会使用它们......

“Pimpl”惯用语是“Pointer to Implement”的缩写,也称为“Compilation Firewall”。现在,让我们深入探讨一下。

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的想法是使用指向实现类的指针,从而不需要包含任何标头:

  • 从而将客户端与依赖项隔离
  • ,从而防止编译连锁反应

另一个好处: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'ed 类:

class Foo
{
public:

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

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

3.更进一步

应该指出的是,虚拟函数的存在通常被视为实现细节,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)) {}

请注意,Foo 的 ABI 完全不关心以下各种更改:可能会发生:

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

因此您的客户端无需担心特定补丁这将添加一个方法或一个属性,并且您无需担心内存布局等......它自然而然地起作用。

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

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<Impl> 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 <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;
}

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<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)) {}

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.

黎夕旧梦 2024-09-23 04:39:33

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

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

With the PIMPL idiom, if the internal implementation details of the IMPL class changes, the clients do not have to be rebuilt. Any change in the interface of the IMPL (and hence header file) class obviously would require the PIMPL class to change.

BTW,
In the code shown, there is a strong coupling between IMPL and PIMPL. So any change in class implementation of IMPL also would cause a need to rebuild.

遇见了你 2024-09-23 04:39:33

考虑一些更现实的事情,好处就会变得更加显着。大多数时候,我将其用于编译器防火墙和实现隐藏,我在可见类所在的同一编译单元中定义实现类。在您的示例中,我不会有 Impl.hImpl.cppPimpl.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 中还有许多未公开的其他可用方法。这也使得该技术非常适合对实现进行分层。

Consider something more realistic and the benefits become more notable. Most of the time that I have used this for compiler firewalling and implementation hiding, I define the implementation class within the same compilation unit that visible class is in. In your example, I wouldn't have Impl.h or Impl.cpp and Pimpl.cpp would look something like:

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

Now no one needs to know about our dependency on boost. This gets more powerful when mixed together with policies. Details like threading policies (e.g., single vs multi) can be hidden by using variant implementations of Impl behind the scenes. Also notice that there are a number of additional methods available in Impl that aren't exposed. This also makes this technique good for layering your implementation.

风吹过旳痕迹 2024-09-23 04:39:33

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

一般来说,任何可以在 Impl 中声明为 private(默认)或 protected 的内容都可以更改,而无需重新编译客户端。

In your example, you can change the implementation of data without having to recompile the clients. This would not be the case without the PImpl intermediary. Likewise, you could change the signature or name of Imlp::DoSomething (to a point), and the clients wouldn't have to know.

In general, anything that can be declared private (the default) or protected in Impl can be changed without recompiling the clients.

帅的被狗咬 2024-09-23 04:39:33

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

私有与您的实现紧密耦合,因此这意味着您的 .hpp 文件确实可以泄露很多有关内部实现的信息。

考虑一下您选择在类内部私有使用的线程库之类的东西。如果不使用 Pimpl,线程类和类型可能会作为私有成员或私有方法上的参数出现。好吧,线程库可能是一个不好的例子,但你明白了:你的类定义的私有部分应该隐藏起来,远离那些包含你的头的人。

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

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

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

In non-Pimpl class headers the .hpp file defines the public and private components of your class all in one big bucket.

Privates are closely coupled to your implementation, so this means your .hpp file really can give away a lot about your internal implementation.

Consider something like the threading library you choose to use privately inside the class. Without using Pimpl, the threading classes and types might be encountered as private members or parameters on private methods. Ok, a thread library might be a bad example but you get the idea: The private parts of your class definition should be hidden away from those who include your header.

That's where Pimpl comes in. Since the public class header no longer defines the "private parts" but instead has a Pointer to Implementation, your private world remains hidden from logic which "#include"s your public class header.

When you change your private methods (the implementation), you are changing the stuff hidden beneath the Pimpl and therefore clients of your class don't need to recompile because from their perspective nothing has changed: They no longer see the private implementation members.

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

一个人练习一个人 2024-09-23 04:39:33

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

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

Not all classes benefit from p-impl. Your example has only primitive types in its internal state which explains why there's no obvious benefit.

If any of the members had complex types declared in another header, you can see that p-impl moves the inclusion of that header from your class's public header to the implementation file, since you form a raw pointer to an incomplete type (but not an embedded field nor a smart pointer). You could just use raw pointers to all your member variables individually, but using a single pointer to all the state makes memory management easier and improves data locality (well, there's not much locality if all those types use p-impl in turn).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文