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

Справка:

Идиома PIMPL (Указатель на IMPLementation) - это метод для сокрытия реализации, в котором открытый класс оборачивает структуру или класс, который не виден за пределами библиотеки, частью которой является открытый класс.

Это скрывает внутренние детали реализации и данные от пользователя библиотеки.

При реализации этой идиомы зачем размещать открытые методы в классе pimpl, а не в публичном классе, поскольку реализации метода открытых классов будут скомпилированы в библиотеку, а у пользователя будет только заголовочный файл?

Чтобы проиллюстрировать, этот код ставит Purr() реализация на классе ImpL и оборачивает его также.

Почему бы не реализовать Пурр непосредственно на публике?

// 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");
}

11 ответов

Решение
  • Потому что ты хочешь Purr() чтобы иметь возможность использовать частные члены CatImpl, Cat::Purr() не будет допущен такой доступ без friend декларация.
  • Потому что тогда вы не смешиваете обязанности: один класс реализует, один класс форвардов.

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

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

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

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

Мы сделали это с перезаписью продукта IONAs Orbix 3.3 в 2000 году.

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

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

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

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

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

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

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

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

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

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

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

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

Если это простой класс, обычно нет причин использовать 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 для эмуляции аспектно-ориентированного программирования, в котором аспекты pre, post и error вызываются до и после выполнения функции-члена.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам