Рассмотрим следующее:
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, клиенты должны перекомпилировать.
Какие виды редактирования здесь дают нам преимущества в плане нерекомпиляции клиентского кода?
Основное преимущество заключается в том, что клиенты интерфейса не обязаны включать заголовки для всех внутренних зависимостей вашего класса. Таким образом, любые изменения в этих заголовках не приводят к перекомпиляции большей части вашего проекта. Плюс общий идеализм по поводу реализации-скрытия.
Кроме того, вам не обязательно размещать свой класс реализации в собственном заголовке. Просто сделайте его структурой внутри одного 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();
}
Рассмотрите что-то более реалистичное, и преимущества станут более заметными. В большинстве случаев, когда я использовал это для брандмауэра компилятора и сокрытия реализации, я определял класс реализации в той же единице компиляции, в которой находится видимый класс. В вашем примере у меня не было бы 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
есть ряд дополнительных методов, которые не предоставляются. Это также делает этот метод хорошим для наслоения вашей реализации.
В вашем примере вы можете изменить реализацию данных
без перекомпиляции клиентов. Этого бы не произошло без посредника PImpl. Точно так же вы можете изменить сигнатуру или имя Imlp::DoSomething
(до определенного предела), и клиенты не должны будут об этом знать.
В общем, все, что можно объявить private
(по умолчанию) или protected
в Impl
, можно изменить без перекомпиляции клиентов.
С идиомой PIMPL, если детали внутренней реализации класса IMPL изменяются, клиенты не должны быть перестроены. Любое изменение интерфейса класса IMPL (и, следовательно, файла заголовка), очевидно, потребует изменения класса PIMPL.
Кстати, В показанном коде существует сильная связь между IMPL и PIMPL. Таким образом, любое изменение в реализации класса IMPL также вызовет необходимость перестроения.
Не все классы выигрывают от p-impl. Ваш пример имеет только примитивные типы во внутреннем состоянии, что объясняет, почему нет очевидной выгоды.
Если какой-либо член имеет сложные типы, объявленные в другом заголовке, вы можете видеть, что p-impl перемещает включение этого заголовка из общедоступного заголовка вашего класса в файл реализации, поскольку вы формируете необработанный указатель на неполный тип ( но не встроенное поле и не интеллектуальный указатель). Вы могли бы просто использовать необработанные указатели на все ваши переменные-члены по отдельности, но использование одного указателя на все состояние упрощает управление памятью и улучшает локальность данных (ну, если все эти типы используют p-impl по очереди, локальности не так много).
В заголовках классов не Pimpl файл .hpp определяет общедоступные и частные компоненты вашего класса в одном большом сегменте.
Privates тесно связаны с вашей реализацией, так что это означает, что ваш файл .hpp действительно может многое рассказать о вашей внутренней реализации.
Рассмотрим что-то вроде библиотеки потоков, которую вы выбираете для частного использования внутри класса. Без использования Pimpl классы и типы потоков могут встречаться как частные члены или параметры в частных методах. Хорошо, библиотека потоков может быть плохим примером, но вы поняли: частные части определения вашего класса должны быть скрыты от тех, кто включает ваш заголовок.
Вот тут-то и появляется Pimpl. Поскольку заголовок общедоступного класса больше не определяет «частные части», а вместо этого имеет указатель на реализацию, ваш частный мир остается скрытым от логики, которая «#include» является вашей. заголовок общедоступного класса.
Когда вы меняете свои приватные методы (реализация), вы меняете вещи, скрытые под Pimpl, и поэтому клиентам вашего класса не нужно перекомпилировать, потому что с их точки зрения ничего не изменилось: они больше не видят частные члены реализации.