Было несколько ответов... но до сих пор нет правильной реализации. Я несколько опечален тем, что примеры неверны, поскольку люди, вероятно, будут их использовать...
Идиома «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); }
Что в нем есть такого, чего не было в других?
- Он просто подчиняется правилу трех: определение конструктора копирования, оператора присваивания копирования и деструктора.
- Он реализует Strong Guarantee: если во время присваивания копия выдает исключение, объект остается неизменным. Обратите внимание, что деструктор
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
равен размеру простого указателя, на что бы он ни указывал
Поэтому вашему клиенту не нужно беспокоиться о конкретном патче, который добавит либо метод, либо атрибут, и вам не нужно беспокоиться о расположении памяти и т. Д. ... это просто естественно работает.
person
Matthieu M.
schedule
30.08.2010