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

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

8
задан Billy ONeal 29 August 2010 в 16:40
поделиться

6 ответов

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

Кроме того, вам не обязательно размещать свой класс реализации в собственном заголовке. Просто сделайте его структурой внутри одного 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();
}
10
ответ дан 3 November 2019 в 13:35
поделиться

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

4
ответ дан 3 November 2019 в 13:35
поделиться

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

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

3
ответ дан 3 November 2019 в 13:35
поделиться

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

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

5
ответ дан 3 November 2019 в 13:35
поделиться

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

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

1
ответ дан 3 November 2019 в 13:35
поделиться

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

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

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

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

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

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

1
ответ дан 3 November 2019 в 13:35
поделиться
Другие вопросы по тегам:

Похожие вопросы: