Справочная информация:
PIMPL Идиома (указатель на IMPLementation) - это метод скрытия реализации, при котором открытый класс обертывает структуру или класс, которые нельзя увидеть за пределами библиотеки, частью которой является открытый класс.
Это скрывает детали внутренней реализации и данные от пользователя библиотеки.
При реализации этой идиомы, почему вы должны размещать общедоступные методы в классе pimpl, а не в общедоступном классе, поскольку реализации методов общедоступных классов будут скомпилированы в библиотеку, а у пользователя есть только файл заголовка?
Чтобы проиллюстрировать, этот код помещает реализацию Purr() в класс impl и также обертывает его.
Почему бы не реализовать Purr непосредственно в общедоступном классе?
// header file:
class Cat {
private:
class CatImpl; // Not defined here
CatImpl *cat_; // Handle
public:
Cat(); // Constructor
~Cat(); // Destructor
// Other operations...
Purr();
};
// CPP file:
#include "cat.h"
class Cat::CatImpl {
Purr();
... // The actual implementation can be anything
};
Cat::Cat() {
cat_ = new CatImpl;
}
Cat::~Cat() {
delete cat_;
}
Cat::Purr(){ cat_->Purr(); }
CatImpl::Purr(){
printf("purrrrrr");
}
Отличный ответ, и я обнаружил, что эта ссылка также содержит исчерпывающую информацию: marcmutz.wordpress.com/translated-articles/pimp-my-pimpl
Если вы хотите, чтобы кодировщик техобслуживания оказал вам услугу, помните, что это шаблон интерфейс. Не используйте его для каждого внутреннего класса. Процитируя «Бегущего по лезвию», я видел дерьмо, в которое вы не поверите.
будьте осторожны, PIMPL может иметь много преимуществ, особенно в крупных проектах, но может серьезно усложнить простую в остальном меньшую программу. Где-то в конце этого вопроса был список минимальных «предпосылок» для использования PIMPL в проекте. Не всем следует следовать одному и тому же списку, составлять его для себя и придерживаться его. На мой взгляд, это, наверное, лучший способ сделать это.
По моему собственному опыту, pimpl предпочитают люди, создающие большие недокументированные фреймворки, а затем уходящие из компании, поэтому их бывшим коллегам приходится иметь дело с классами, которые очень сложно анализировать ...





Размещение вызова impl-> Purr внутри файла cpp означает, что в будущем вы можете делать что-то совершенно другое без изменения файла заголовка. Возможно, в следующем году они обнаружат вспомогательный метод, который они могли бы вызвать вместо этого, и поэтому они смогут изменить код, чтобы вызывать его напрямую, а не использовать impl-> Purr вообще. (Да, они могли бы добиться того же, обновив фактический метод impl :: Purr, но в этом случае вы застряли с дополнительным вызовом функции, который ничего не дает, кроме вызова следующей функции по очереди)
Это также означает, что заголовок имеет только определения и не имеет какой-либо реализации, которая обеспечивает более четкое разделение, что составляет всю суть идиомы.
Я думаю, что большинство людей называют это идиомой «Ручка тела». См. Книгу Джеймса Коплиена «Расширенные стили и идиомы программирования на C++» (Ссылка на Amazon). Он также известен как Чеширский кот из-за персонажа Льюиса Кэролла, который исчезает, пока не остается только ухмылка.
Код примера следует распределить по двум наборам исходных файлов. Тогда только Cat.h - это файл, который поставляется с продуктом.
CatImpl.h включен в Cat.cpp, а CatImpl.cpp содержит реализацию CatImpl :: Purr (). Это не будет видно широкой публике, использующей ваш продукт.
В основном идея состоит в том, чтобы максимально скрыть реализацию от посторонних глаз. Это наиболее полезно, когда у вас есть коммерческий продукт, который поставляется в виде серии библиотек, доступ к которым осуществляется через API, с которым скомпилирован и связан код клиента.
Мы сделали это, переписав продукт IONAs Orbix 3.3 в 2000 году.
Как упоминалось другими, использование его техники полностью отделяет реализацию от интерфейса объекта. Тогда вам не придется перекомпилировать все, что использует Cat, если вы просто хотите изменить реализацию Purr ().
Этот метод используется в методологии под названием дизайн по контракту.
Как идиома pimpl (или идиома ручки-тела, как вы ее называете) используется в дизайне по контракту?
Привет, Андреас! Ваша точка взаимодействия с пользователем API находится только в открытом контракте (дескрипторе), а не в том, как вы реализуете тело для обеспечения заявленной функциональности. Вы можете изменить реализацию по своему усмотрению при условии, что вы не измените семантику рекламируемого API.
@ Роб, я полагаю, там очень мало накладных расходов. Дополнительный класс, но их очень мало. Просто тонкая обертка вокруг существующего класса. Кто-то поправит меня, если я ошибаюсь, но использование памяти будет просто дополнительной таблицей функций в ОЗУ, указателем на pimpl и функцией перенаправления для каждого метода в кодовом пространстве. Хотя это больно для обслуживания и отладки.
@RobWells Я отклонил этот ответ, потому что он тоже неверен (но не так ошибочен, как принятый; этот ответ потенциально можно исправить). Вопросы: а) «В основном идея состоит в том, чтобы максимально скрыть реализацию от посторонних глаз». Чем он отличается от простого разделения объявления / определения классов .h / .cpp и библиотеки доставки как .h / (. A | .lib)? Это также, очевидно, скрывает реализацию от клиента. OP упомянул этот явно в вопросе!
б) «Мы сделали это, переписав продукт IONAs Orbix 3.3 в 2000 году». Думаю, это круто. c) «использование его техники полностью отделяет реализацию от интерфейса объекта. Тогда вам не придется перекомпилировать все, что использует Cat, если вы просто хотите изменить реализацию Purr ()». Это неправда. Изменение реализации метода никогда не вызывает перекомпиляцию. Потребуется повторная ссылка. Но разве это удивительно? Вы изменили код, хотите использовать новую версию.
г) «Он также известен как Чеширский кот из-за характера Льюиса Кэролла, который исчезает, пока не останется только ухмылка». Источник, пожалуйста? д) «Этот метод используется в методологии, называемой проектированием по контракту». Источник, пожалуйста?
FFR: в основном обратитесь к принятому ответу в stackoverflow.com/questions/8972588/…
Как правило, единственной ссылкой на класс Pimpl в заголовке класса Owner (в данном случае Cat) будет предварительное объявление, как вы это сделали здесь, потому что это может значительно уменьшить зависимости.
Например, если ваш класс Pimpl имеет ComplicatedClass в качестве члена (а не только указатель или ссылку на него), вам нужно будет полностью определить ComplicatedClass перед его использованием. На практике это означает включение ComplicatedClass.h (который также косвенно будет включать все, от чего зависит ComplicatedClass). Это может привести к тому, что при заполнении одного заголовка потребуется много-много всего, что плохо для управления вашими зависимостями (и временем компиляции).
Когда вы используете pimpl idion, вам нужно только # включить то, что используется в общедоступном интерфейсе вашего типа Owner (которым здесь будет Cat). Это облегчает жизнь людям, использующим вашу библиотеку, и означает, что вам не нужно беспокоиться о людях, зависящих от какой-то внутренней части вашей библиотеки - либо по ошибке, либо потому, что они хотят сделать что-то, что вы не разрешаете, поэтому они #define закрытый публичный перед включением ваших файлов.
Если это простой класс, обычно нет причин использовать Pimpl, но для случаев, когда типы довольно большие, это может быть большим подспорьем (особенно во избежание длительного времени сборки).
Purr() мог использовать закрытые члены CatImpl. Cat::Purr() не будет разрешен такой доступ без декларации friend.Хотя поддерживать это - боль. Но опять же, если это библиотечный класс, методы в любом случае не следует сильно менять. Код, на который я смотрю, похоже, выбирает безопасный путь и повсюду использует pimpl.
Разве эта строка не является незаконной, поскольку все члены являются закрытыми: cat _-> Purr (); Purr () недоступен извне, потому что по умолчанию он закрыт. Что мне здесь не хватает?
Оба пункта не имеют никакого смысла. Если бы у вас был один класс - Cat, он также имел бы доступ к своим членам и не «смешивал бы возможности повторения», потому что это был бы класс, который «реализует». Причины использования PIMPL разные.
Этот ответ просто неверен по причинам, упомянутым @doc. Было бы абсурдно вводить новый класс только ради использования его закрытых членов, и просто пересылать что-то не является «обязанностью»!
Если ваш класс использует идиому pimpl, вы можете избежать изменения файла заголовка в общедоступном классе.
Это позволяет добавлять / удалять методы в класс pimpl без изменения файла заголовка внешнего класса. Вы также можете добавить / удалить #includes в pimpl.
Когда вы изменяете файл заголовка внешнего класса, вам необходимо перекомпилировать все, что # включает его (и если какой-либо из них является заголовочными файлами, вам нужно перекомпилировать все, что # включает их, и так далее)
Не знаю, стоит ли упоминать об этой разнице, но ...
Можно ли иметь реализацию в собственном пространстве имен и иметь общедоступное пространство имен оболочки / библиотеки для кода, который видит пользователь:
catlib::Cat::Purr(){ cat_->Purr(); }
cat::Cat::Purr(){
printf("purrrrrr");
}
Таким образом, весь код библиотеки может использовать пространство имен cat, и, поскольку возникает необходимость предоставить класс пользователю, можно создать оболочку в пространстве имен catlib.
Я считаю, что это говорит о том, что, несмотря на то, насколько хорошо известна идиома pimpl, я не вижу, чтобы она часто возникала в реальной жизни (например, в проектах с открытым исходным кодом).
Я часто задаюсь вопросом, не преувеличены ли «преимущества»; да, вы можете сделать некоторые детали вашей реализации еще более скрытыми, и да, вы можете изменить свою реализацию, не меняя заголовок, но не очевидно, что в действительности это большие преимущества.
То есть неясно, есть ли какая-либо необходимость в том, чтобы ваша реализация была хорошо скрыта который, и, возможно, довольно редко люди действительно меняют только реализацию; как только вам нужно добавить новые методы, скажем, вам все равно нужно изменить заголовок.
Да, это имеет значение только в том случае, если вы можете выбрать хороший публичный интерфейс и придерживаться его. Как упоминает Роб Уэллс, это важно, если вам нужно распространять обновленные библиотеки среди людей, связывающихся с скомпилированными версиями вашей библиотеки, не заставляя их перекомпилировать - вы просто предоставляете новую DLL и вуаля. Имейте в виду, что использование интерфейсов (== абстрактные классы без членов данных в C++) позволяет добиться того же самого с меньшими затратами на обслуживание (нет необходимости вручную перенаправлять каждый общедоступный метод). Но OTOH вы должны использовать специальный синтаксис для создания экземпляров (т.е. вызывать фабричный метод).
Qt широко использует идиому PIMPL.
Я только что реализовал свой первый урок сутенера за последние пару дней. Я использовал его для устранения проблем, которые у меня были, включая winsock2.h в Borland Builder. Казалось, что это мешает выравниванию структуры, и, поскольку у меня были сокеты в частных данных класса, эти проблемы распространялись на любой файл cpp, который включал заголовок.
Используя pimpl, winsock2.h был включен только в один файл cpp, где я мог закрыть проблему и не беспокоиться о том, что она вернется, чтобы укусить меня.
Чтобы ответить на исходный вопрос, преимущество, которое я обнаружил при перенаправлении вызовов в класс pimpl, заключалось в том, что класс pimpl такой же, как и ваш исходный класс до того, как вы его добавили, плюс ваши реализации не распространяются на 2 классы каким-то странным образом. Гораздо понятнее реализовать публикацию, которая будет просто перенаправлять в класс pimpl.
Как сказал мистер Ноде, один класс, одна ответственность.
Что ж, я бы не стал его использовать. У меня есть альтернатива получше:
foo.h:
class Foo {
public:
virtual ~Foo() { }
virtual void someMethod() = 0;
// This "replaces" the constructor
static Foo *create();
}
foo.cpp:
namespace {
class FooImpl: virtual public Foo {
public:
void someMethod() {
//....
}
};
}
Foo *Foo::create() {
return new FooImpl;
}
У этого паттерна есть название?
Как программисту также на Python и Java, мне это нравится намного больше, чем идиома pImpl.
Если вы уже ограничиваетесь фабричным подходом к созданию объектов, тогда это нормально. Но он полностью исключает семантику значений, тогда как традиционный pImpl работает с любым методом.
Ну, pImpl просто оборачивает указатель. Все, что вам нужно сделать, это заставить create () выше возвращать PointerWrapperWithCopySemantics <Foo> :-). Обычно я делаю противоположное и возвращаю std :: auto_ptr <Foo>.
Почему наследование важнее композиции? Зачем добавлять накладные расходы на vtable? Почему виртуальное наследование?
Это не работает с шаблонными методами
Как бы то ни было, он отделяет реализацию от интерфейса. Обычно это не очень важно для небольших проектов. Но в больших проектах и библиотеках его можно использовать для значительного сокращения времени сборки.
Учтите, что реализация Cat может включать в себя множество заголовков, может включать метапрограммирование шаблонов, которое требует времени для самостоятельной компиляции. Почему пользователь, который просто хочет использовать Cat, должен включать все это? Следовательно, все необходимые файлы скрыты с использованием идиомы pimpl (отсюда и прямое объявление CatImpl), и использование интерфейса не заставляет пользователя включать их.
Я разрабатываю библиотеку для нелинейной оптимизации (прочтите «много неприятной математики»), которая реализована в шаблонах, поэтому большая часть кода находится в заголовках. Компиляция занимает около пяти минут (на приличном многоядерном процессоре), а простой анализ заголовков в пустом .cpp занимает около минуты. Таким образом, любой, кто использует библиотеку, должен ждать пару минут каждый раз, когда компилирует свой код, что делает разработку довольно скучный. Однако, скрывая реализацию и заголовки, можно просто включить простой интерфейсный файл, который мгновенно компилируется.
Это не обязательно имеет какое-либо отношение к защите реализации от копирования другими компаниями - что, вероятно, не произойдет в любом случае, если только внутренняя работа вашего алгоритма не может быть угадана из определений переменных-членов (если да, то это вероятно, не очень сложный и не заслуживающий защиты в первую очередь).
Мы используем идиому PIMPL для имитации аспектно-ориентированного программирования, в котором аспекты до, после и ошибки вызываются до и после выполнения функции-члена.
struct Omg{
void purr(){ cout<< "purr\n"; }
};
struct Lol{
Omg* omg;
/*...*/
void purr(){ try{ pre(); omg-> purr(); post(); }catch(...){ error(); } }
};
Мы также используем указатель на базовый класс, чтобы разделять различные аспекты между многими классами.
Недостатком этого подхода является то, что пользователь библиотеки должен учитывать все аспекты, которые будут выполняться, но видит только свой класс. Это требует просмотра документации на предмет каких-либо побочных эффектов.
Отложив в сторону аргумент о скорости компиляции, это самая полезная вещь, которую предоставляет PIMPL, IMO.
Потому что идиомы PIMP следует избегать? ..