Был ряд ответов ... но правильной реализации пока нет. Я несколько опечален, что примеры неверны, так как люди могут их использовать ...
Идиома «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 является использование указателя на класс реализации, чтобы не нужно включать любой заголовок:
- изолируя таким образом клиент от зависимостей
- таким образом предотвращая компиляции волновой эффект
дополнительный benefi t: сохраняется 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); }
Что есть, что другие не делали?
- Он просто подчиняется Правилу Тройки: определение конструктора копирования, оператора присваивания копий и деструктора.
- Это делает реализацию Strong Guarantee: если копия выбрасывается во время назначения, объект остается неизменным. Обратите внимание, что деструктор
T
не должны бросить ... но потом, что это очень распространенное требование;)
Основываясь на этом, мы можем теперь определить классы Pimpl'ed несколько легко:
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
является совершенно не заботясь различные изменения, которые могут произойти:
- нет виртуального меня ThOD в
Foo
- размер
mImpl
является то, что простой указатель, независимо от того, что он указывает на
Поэтому ваш клиент не должен беспокоиться о конкретном участке, который бы добавить либо метод или атрибут, и вам нужно не беспокойтесь о макете памяти и т. д., это просто естественно работает.
+1 - Можете ли вы привести пример? –
Ваш пример вызывает неопределенное поведение: вы забыли «печально известное» правило из трех. Всякий раз, когда вы определяете один из Copy Constructor, Copy Assignment Operator или Destructor, определите два других. –
Не уверен в неопределенном поведении, созданный компилятором конструктор копий хорошо определен, но приведет к возможному двойному освобождению, если он вызван. Или эффект двойного действия означает, что вы имели в виду UB? –