Почему следует использовать идиому PIMPL?

Справочная информация:

Идиома 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");
}

person JeffV    schedule 13.09.2008    source источник
comment
Потому что идиомы PIMP следует избегать? ..   -  person mlvljr    schedule 08.10.2010
comment
Отличный ответ, и я обнаружил, что эта ссылка также содержит исчерпывающую информацию: marcmutz.wordpress .com / переведенные-статьи / pimp-my-pimpl   -  person zhanxw    schedule 18.02.2013
comment
Если вы хотите оказать услугу кодировщику обслуживания, помните, что это шаблон интерфейса. Не используйте его для каждого внутреннего класса. Процитируя «Бегущего по лезвию», я видел дерьмо, в которое вы не поверите.   -  person DevSolar    schedule 11.03.2013
comment
будьте осторожны, PIMPL может иметь много преимуществ, особенно в больших проектах, но может серьезно усложнить в остальном простую небольшую программу. Где-то в конце этого вопроса был список минимальных предпосылок для использования PIMPL в проекте. Не всем следует следовать одному и тому же списку, составлять его для себя и придерживаться его. На мой взгляд, это, наверное, лучший способ сделать это.   -  person osirisgothra    schedule 08.02.2014
comment
По моему собственному опыту, pimpl предпочитают люди, создающие большие недокументированные фреймворки, а затем покидающие компанию, поэтому их бывшим коллегам приходится иметь дело с классами, которые очень сложно анализировать...   -  person Trantor    schedule 24.01.2017


Ответы (11)


  • Поскольку вы хотите, чтобы Purr() мог использовать закрытых членов CatImpl. Cat::Purr() не будет разрешен такой доступ без объявления friend.
  • Потому что тогда вы не смешиваете обязанности: один класс реализует, один класс пересылает.
person Xavier Nodet    schedule 13.09.2008
comment
Хотя поддерживать это - боль. Но опять же, если это библиотечный класс, методы в любом случае не следует сильно менять. Код, на который я смотрю, похоже, выбирает безопасный путь и повсюду использует pimpl. - person JeffV; 13.09.2008
comment
Разве эта строка не является незаконной, поскольку все члены являются закрытыми: cat _- ›Purr (); Purr () недоступен извне, потому что по умолчанию он закрыт. Что мне здесь не хватает? - person binaryguy; 14.08.2015
comment
Оба пункта не имеют никакого смысла. Если бы у вас был один класс - Cat, он также имел бы доступ к своим членам и не смешивал бы возможности повторения, потому что это был бы класс, который реализует. Причины использования PIMPL разные. - person doc; 10.01.2017
comment
Этот ответ просто неверен по причинам, упомянутым @doc. Было бы абсурдно вводить новый класс только ради использования его закрытых членов, и это не обязанность просто пересылать что-то! - person cubuspl42; 11.04.2019

Я думаю, что большинство людей называют это идиомой Handle Body. См. Книгу Джеймса Коплиена Расширенные стили и идиомы программирования C ++ < / а>. Он также известен как Чеширский кот из-за Персонаж Льюиса Кэролла, исчезающий до тех пор, пока не останется только ухмылка.

Код примера следует распределить по двум наборам исходных файлов. Тогда только файл Cat.h будет поставляться с продуктом.

CatImpl.h включен в Cat.cpp, а CatImpl.cpp содержит реализацию для CatImpl :: Purr () . Это не будет видно широкой публике, использующей ваш продукт.

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

Это наиболее полезно, когда у вас есть коммерческий продукт, который поставляется в виде серии библиотек, доступ к которым осуществляется через API, с которым скомпилирован и связан код клиента.

Мы сделали это, переписав IONA Orbix 3.3, выпущенный в 2000 году.

Как упоминалось другими, использование его техники полностью отделяет реализацию от интерфейса объекта. Тогда вам не придется перекомпилировать все, что использует Cat, если вы просто хотите изменить реализацию Purr ().

Этот метод используется в методологии, называемой проектированием по контракту.

person Rob Wells    schedule 13.09.2008
comment
@ Роб, я полагаю, там очень мало накладных расходов. Дополнительный класс, но их очень мало. Просто тонкая обертка вокруг существующего класса. Кто-то поправит меня, если я ошибаюсь, но использование памяти будет просто дополнительной таблицей функций в ОЗУ, указателем на pimpl и функцией перенаправления для каждого метода в кодовом пространстве. Однако болезненно для обслуживания и отладки. - person JeffV; 17.09.2008
comment
Как идиома pimpl (или идиома ручки-тела, как вы ее называете) используется в дизайне по контракту? - person andreas buykx; 23.09.2008
comment
Привет, Андреас! Ваша точка интерфейса для пользователя API находится только в открытом контракте (дескрипторе), а не в том, как вы реализуете тело для предоставления рекламируемой функциональности. Вы можете изменить реализацию по своему усмотрению, при условии, что вы не измените семантику API, который вы рекламируете. - person Rob Wells; 05.12.2014
comment
@RobWells Я отклонил этот ответ, потому что он тоже неверен (но не так плох, как принятый; этот ответ потенциально можно исправить). Проблемы: а) В основном идея состоит в том, чтобы максимально скрыть реализацию от посторонних глаз. Чем он отличается от простого разделения объявления / определения классов .h / .cpp и библиотеки доставки как .h / (. A | .lib)? Это также, очевидно, скрывает реализацию от клиента. ОП упомянул об этом явно в вопросе! - person cubuspl42; 11.04.2019
comment
б) Мы сделали это, переписав продукт IONAs Orbix 3.3 в 2000 году. Думаю, это круто. c) использование его техники полностью отделяет реализацию от интерфейса объекта. Тогда вам не придется перекомпилировать все, что использует Cat, если вы просто хотите изменить реализацию Purr (). Это неправда. Изменение реализации метода никогда не вызывает перекомпиляцию. Потребуется повторная ссылка. Но разве это удивительно? Вы изменили код, хотите, чтобы использовалась новая версия. - person cubuspl42; 11.04.2019
comment
г) Он также известен как Чеширский Кот из-за персонажа Льюиса Кэролла, который исчезает, пока не остается только ухмылка. Источник, пожалуйста? д) Этот метод используется в методологии, называемой «проектирование по контракту». Источник, пожалуйста? - person cubuspl42; 11.04.2019
comment
FFR: в основном обратитесь к принятому ответу в stackoverflow.com/questions/8972588/ - person cubuspl42; 11.04.2019

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

Учтите, что реализация Cat может включать в себя множество заголовков, может включать метапрограммирование шаблонов, которое требует времени для самостоятельной компиляции. Почему пользователь, который просто хочет использовать Cat, должен включать все это? Следовательно, все необходимые файлы скрыты с использованием идиомы pimpl (отсюда и прямое объявление CatImpl), и использование интерфейса не заставляет пользователя включать их.

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

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

person the swine    schedule 23.09.2014

Если ваш класс использует идиому PIMPL, вы можете избежать изменения файла заголовка в общедоступном классе.

Это позволяет добавлять / удалять методы в класс PIMPL без изменения файла заголовка внешнего класса. Вы также можете добавить / удалить #includes в PIMPL.

Когда вы меняете файл заголовка внешнего класса, вам нужно перекомпилировать все, что # включает его (и если какие-либо из них являются заголовочными файлами, вам нужно перекомпилировать все, что # включает их, и так далее).

person Nick    schedule 13.09.2008

Как правило, единственная ссылка на класс PIMPL в заголовке для класса owner (в данном случае Cat) будет предварительным объявлением, как вы сделали здесь, потому что это может значительно уменьшить зависимости.

Например, если ваш класс PIMPL имеет ComplicatedClass в качестве члена (а не только указатель или ссылку на него), вам потребуется полностью определить ComplicatedClass перед его использованием. На практике это означает включение файла ComplicatedClass.h (который также косвенно будет включать все, от чего зависит ComplicatedClass). Это может привести к тому, что при заполнении одного заголовка потребуется много-много всего, что плохо для управления вашими зависимостями (и временем компиляции).

Когда вы используете идиому PIMPL, вам нужно только #include использовать материал, используемый в общедоступном интерфейсе вашего типа owner (здесь это будет Cat). Это облегчает жизнь людям, использующим вашу библиотеку, и означает, что вам не нужно беспокоиться о том, что люди зависят от какой-то внутренней части вашей библиотеки — либо по ошибке, либо потому, что они хотят сделать что-то, что вы не разрешаете, поэтому они # определите частную публичную перед включением ваших файлов.

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

person Wilka    schedule 13.09.2008

Что ж, я бы не стал им пользоваться. У меня есть альтернатива получше:

Файл 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.

person Esben Nielsen    schedule 21.06.2011
comment
Если вы уже ограничиваетесь фабричным подходом к созданию объектов, тогда это нормально. Но он полностью исключает семантику значений, тогда как традиционный pImpl работает с любым методом. - person Dennis Zickefoose; 21.06.2011
comment
Ну, pImpl просто оборачивает указатель. Все, что вам нужно сделать, это заставить create () выше возвращать PointerWrapperWithCopySemantics ‹Foo› :-). Я обычно делаю противоположное и возвращаю std :: auto_ptr ‹Foo›. - person Esben Nielsen; 22.06.2011
comment
Почему наследование важнее композиции? Зачем добавлять накладные расходы на vtable? Почему виртуальное наследование? - person derpface; 11.10.2013
comment
Это не работает с шаблонными методами - person smac89; 19.02.2018

Мы используем идиому PIMPL для имитации аспектно-ориентированного программирования, где предварительное, пост-и Аспекты ошибок вызываются до и после выполнения функции-члена.

struct Omg{
   void purr(){ cout<< "purr\n"; }
};

struct Lol{
  Omg* omg;
  /*...*/
  void purr(){ try{ pre(); omg-> purr(); post(); }catch(...){ error(); } }
};

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

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

person nurettin    schedule 11.11.2014
comment
Отложив в сторону аргумент о скорости компиляции, это самая полезная вещь, которую предоставляет PIMPL, IMO. - person John Z. Li; 20.02.2019

Размещение вызова метода impl- ›Purr внутри файла .cpp означает, что в будущем вы сможете делать что-то совершенно другое без изменения файла заголовка.

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

Это также означает, что заголовок имеет только определения и не имеет какой-либо реализации, которая обеспечивает более четкое разделение, что составляет всю суть идиомы.

person Phil Wright    schedule 13.09.2008

Я только что реализовал свой первый класс PIMPL за последние пару дней. Я использовал его для устранения возникших у меня проблем, включая файл * winsock2. * H в Borland Builder. Казалось, что это мешает выравниванию структуры, и, поскольку у меня были сокеты в частных данных класса, эти проблемы распространялись на любой файл .cpp, который включал заголовок.

Используя PIMPL, winsock2.h был включен только в один файл .cpp, где я мог закрыть проблему и не волноваться, что она снова укусит меня.

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

Как сказал г-н Нодет, один класс, одна ответственность .

person markh44    schedule 24.03.2009

Не знаю, стоит ли упоминать об этой разнице, но ...

Можно ли иметь реализацию в собственном пространстве имен и иметь общедоступное пространство имен оболочки / библиотеки для кода, который видит пользователь:

catlib::Cat::Purr(){ cat_->Purr(); }
cat::Cat::Purr(){
   printf("purrrrrr");
}

Таким образом, весь код библиотеки может использовать пространство имен cat, и, поскольку возникает необходимость предоставить класс пользователю, можно создать оболочку в пространстве имен catlib.

person JeffV    schedule 13.09.2008

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

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

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

person DrPizza    schedule 15.09.2008
comment
Да, это имеет значение только в том случае, если вы можете выбрать хороший общедоступный интерфейс и придерживаться его. Как упоминает Роб Уэллс, это важно, если вам нужно распространять обновленные библиотеки среди людей, связывающихся с скомпилированными версиями вашей библиотеки, не заставляя их перекомпилировать - вы просто предоставляете новую DLL и вуаля. Имейте в виду, что использование интерфейсов (== абстрактные классы без членов данных в C ++) позволяет добиться того же самого с меньшими затратами на обслуживание (нет необходимости вручную перенаправлять каждый общедоступный метод). Но OTOH вы должны использовать специальный синтаксис для создания экземпляров (т.е. вызывать фабричный метод). - person j_random_hacker; 22.09.2009
comment
Qt широко использует идиому PIMPL. - person el.pescado; 09.01.2010