2010-08-30 1 views
8

Рассмотрим следующий пример:Как идиома pimpl уменьшает зависимости?

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 или no pimpl, клиенты должны перекомпилировать.

Какие виды редактирования здесь дают нам преимущества с точки зрения не перекомпиляции кода клиента?

ответ

10

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

Кроме того, вы не обязательно будете класть свой класс impl в свой собственный заголовок. Просто сделайте его структурой внутри одного cpp и сделайте свой внешний класс ссылкой на своих членов данных напрямую.

Edit: Пример

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

+1 - Можете ли вы привести пример? –

+2

Ваш пример вызывает неопределенное поведение: вы забыли «печально известное» правило из трех. Всякий раз, когда вы определяете один из Copy Constructor, Copy Assignment Operator или Destructor, определите два других. –

+1

Не уверен в неопределенном поведении, созданный компилятором конструктор копий хорошо определен, но приведет к возможному двойному освобождению, если он вызван. Или эффект двойного действия означает, что вы имели в виду UB? –

5

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

BTW, В показанном коде есть сильная связь между IMPL и PIMPL. Таким образом, любое изменение в реализации класса IMPL также вызовет необходимость перестроить.

+0

Err .. разве это не так с шаблоном .cpp и .hpp? Если реализация в .cpp изменяется, .hpp делает * not * change. Следовательно, никакой другой код не нужно перекомпилировать .... –

+2

Реализация включает в себя элементы данных, такие как 'data' и частные методы. Они бы изменили 'Impl.h' (если он существовал), но не изменяют' PImpl.h'. – Beta

+0

@Bill ONeal: Кроме того, поскольку PIMPL является непрозрачным указателем, его можно указать на любой конкретный производный класс из абстракции PIMPL, тем самым получая преимущества шаблона проектирования стратегии. Вы больше не зависите от конкретных Concrete Strategies, а от интерфейса. На мой взгляд, эта идиома в основном схожа с двумя принципами OOAD: а) Программа для интерфейса, а не для реализации; б) Упорядочить агрегацию над наследованием. Правильно ли я понимаю? – Chubsdad

3

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

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

4

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

1

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

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

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

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

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

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

1

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

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

+0

Я не согласен, даже с примитивными типами в реализации с использованием Pimpl позволяет изменять их без каких-либо инцидентов на ваших клиентах (ABI сохранена, не требуется перекомпиляции). –

5

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

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

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

+0

+1: для получения идиомы из первых принципов – Chubsdad

+0

+1 - Какое обстоятельство вы когда-либо были бы, хотя бы где бы вы хотели получить доступ к объектам «pimpl», но не хотели бы получать доступ к объектам 'impl' ? И в таких случаях, как это отличается от прямого объявления 'impl' и хранения его в классе интеллектуальных указателей? –

+0

@Billy: не понимайте всех ... Основное отличие от классического умного указателя состоит в том, что 'pimpl' реализует семантику Deep Copy в сильной гарантии, освобождая пользователя от этого бремени. В противном случае он очень похож на 'scoped_ptr'. –

 Смежные вопросы

  • Нет связанных вопросов^_^