Как идиома pimpl уменьшает зависимости? - PullRequest
8 голосов
/ 30 августа 2010

Рассмотрим следующее:

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 ]

10 голосов
/ 30 августа 2010

Основным преимуществом является то, что клиенты интерфейса не обязаны включать заголовки для всех внутренних зависимостей вашего класса.Таким образом, любые изменения в этих заголовках не касаются перекомпиляции большей части вашего проекта.Плюс общий идеализм о сокрытии реализации.

Кроме того, вы не обязательно помещаете свой класс 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();
}
6 голосов
/ 30 августа 2010

Было несколько ответов ... но пока нет правильной реализации. Я несколько опечален тем, что примеры неверны, так как люди могут их использовать ...

Идиома «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 соответствует размеру простого указателя, на что бы он ни указывал

Поэтому вашему клиенту не нужно беспокоиться о конкретном патче, который добавит либо метод, либо атрибут, и вам не нужно беспокоиться о расположении памяти и т. Д. ... это просто естественно работает.

5 голосов
/ 30 августа 2010

С идиомой PIMPL, если внутренние детали реализации класса IMPL изменяются, клиенты не должны быть перестроены. Любое изменение в интерфейсе класса IMPL (и, следовательно, файла заголовка), очевидно, потребовало бы изменения класса PIMPL.

Кстати, В показанном коде между IMPL и PIMPL существует сильная связь. Поэтому любое изменение в реализации класса IMPL также может привести к необходимости перестройки.

4 голосов
/ 30 августа 2010

Рассмотрите что-то более реалистичное, и преимущества станут более заметными.В большинстве случаев, когда я использовал это для межсетевого экрана компилятора и скрытия реализации, я определяю класс реализации в том же модуле компиляции, в котором находится видимый класс. В вашем примере у меня не было бы Impl.h или 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.Это становится более мощным, когда смешивается с политиками.Такие детали, как политики потоков (например, single vs multi), могут быть скрыты с помощью вариантов реализации Impl за кулисами.Также обратите внимание, что в Impl есть ряд дополнительных методов, которые не представлены.Это также делает эту технику хорошей для многоуровневой реализации.

3 голосов
/ 30 августа 2010

В вашем примере вы можете изменить реализацию data без перекомпиляции клиентов.Это было бы не так без посредника PImpl.Аналогично, вы можете изменить подпись или имя Imlp::DoSomething (на точку), и клиенты не должны будут знать.

В общем, все, что может быть объявлено private (по умолчанию)или protected в Impl можно изменить без перекомпиляции клиентов.

1 голос
/ 30 августа 2010

Не все классы получают пользу от p-impl. Ваш пример имеет только примитивные типы во внутреннем состоянии, что объясняет, почему нет очевидной выгоды.

Если у какого-либо из членов были сложные типы, объявленные в другом заголовке, вы можете видеть, что p-impl перемещает включение этого заголовка из открытого заголовка вашего класса в файл реализации, поскольку вы формируете необработанный указатель на неполный тип ( но не встроенное поле и не умный указатель). Вы можете просто использовать необработанные указатели на все переменные-члены по отдельности, но использование единого указателя на все состояния упрощает управление памятью и улучшает локальность данных (ну, если не все эти типы используют p-impl по очереди, то их немного).

1 голос
/ 30 августа 2010

В заголовках классов, не относящихся к Pimpl , файл .hpp определяет открытые и закрытые компоненты вашего класса в одном большом контейнере.

Рядовые элементы тесно связаны с вашей реализацией, поэтому это означает, что ваш файл .hpp действительно может многое рассказать о вашей внутренней реализации.

Рассмотрим что-то вроде библиотеки потоков Вы решили использовать в классе в частном порядке.Без использования Pimpl классы и типы потоков могут встречаться как закрытые члены или параметры в закрытых методах.Хорошо, библиотека потоков может быть плохим примером, но вы понимаете: частные части вашего определения класса должны быть скрыты от тех, кто включает ваш заголовок.

Вот где приходит Pimpl. Так как открытый классзаголовок больше не определяет «приватные части», а вместо этого имеет указатель на реализацию , ваш личный мир остается скрытым от логики, которая «#include» является вашим заголовком открытого класса.

При измененииваши личные методы (реализация), вы изменяете материал, скрытый под Pimpl, и поэтому клиентам вашего класса не нужно перекомпилировать, потому что с их точки зрения ничего не изменилось: они больше не видят частную реализациючлены.

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

...