Было несколько ответов ... но пока нет правильной реализации. Я несколько опечален тем, что примеры неверны, так как люди могут их использовать ...
Идиома «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 является то, что у нас есть правильная структура, позволяющая использовать всю мощь паттерна стратегии.
Для этого необходимо изменить «копию» 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)) {}
Обратите внимание, что ABI Foo
полностью не связан с различными изменениями, которые могут произойти:
- В
Foo
- размер
mImpl
соответствует размеру простого указателя, на что бы он ни указывал
Поэтому вашему клиенту не нужно беспокоиться о конкретном патче, который добавит либо метод, либо атрибут, и вам не нужно беспокоиться о расположении памяти и т. Д. ... это просто естественно работает.