Comments 19
Мне понравилось, как pImpl позволяет скрыть реализацию.
К примеру, взять класс таймера, с разной реализацей для windows и linux
Можно конечно сделать так:
Но с pImpl'ом интереснее:
К примеру, взять класс таймера, с разной реализацей для windows и linux
Можно конечно сделать так:
xTimer* timer = xTimer::Create();
Но с pImpl'ом интереснее:
glAttachShader(pImpl->mProgram, pImpl->mVertexShader->pImpl->mName);
ой, отправилось как-то o_O предыдущий комментарий читать без последней строчки
c pImpl'ом интереснее
Но когда я у себя в коде написал
задумался, наверное что-то не так…
Как-то это можно красиво разрулить?
Реализация gpu-программы использует вершинный шейдер и должна знать о его реализации.
c pImpl'ом интереснее
xTimer* timer = new xTimer()
Но когда я у себя в коде написал
glAttachShader(pImpl->mProgram, pImpl->mVertexShader->pImpl->mName);
задумался, наверное что-то не так…
Как-то это можно красиво разрулить?
Реализация gpu-программы использует вершинный шейдер и должна знать о его реализации.
Как вариант, можно попробовать перегружать оператор -> и возвращать в нем pImpl. Тогда вроде бы получится так:
Но вроде бы потеряется возможность использования переменных и методов mVertexShader снаружи класса (хотя может быть, они будут доступны через (*pImpl->mVertexShader).qwerty()). Точно не знаю, никогда не перегружал этот оператор.
glAttachShader(pImpl->mProgram, pImpl->mVertexShader->mName);
Но вроде бы потеряется возможность использования переменных и методов mVertexShader снаружи класса (хотя может быть, они будут доступны через (*pImpl->mVertexShader).qwerty()). Точно не знаю, никогда не перегружал этот оператор.
Обращение к непосредственно к полю? Зачем тут pimpl тогда, если он нужен, чтобы «интерфейс+указатель» запихнуть в .h, а реализацию в.срр и не париться с перекомпиляцией десятка модулей, которые инклудят .h при изменении реализации (например, когда в реализацию добавляется пара полей — abi не меняется). Если pimpl — public член, и клиент пишет attach(foo.pimpl->name), то не понятно, зачем весь этот огород. Что нужно сделать — не обращаться к полям напрямую и написать в интерфейсе геттер getName(){return pimpl->name;}
Обращение внутри реализации и только.
Getter'ы не совсем-то, попробую объяснить, name — это специфичное для opengl-реализации поле(хендл ресурса), в d3d реализации например указатель на интерфейс.
Getter'ы не совсем-то, попробую объяснить, name — это специфичное для opengl-реализации поле(хендл ресурса), в d3d реализации например указатель на интерфейс.
Обращение внутри реализации и только.
Внутри реализации не нужно было бы писать pimpl->mProgram, это же сам impl и есть, а значит нужно писать просто mProgram или там this->mProgram.
Если под реализацией вы понимаете .cpp файл класса-обертки, то в нем нужно переадресовывать все в реализацию, а не просто использовать Impl-класс как хранилище приватных полей. То есть так:
void Class::AttachShader(){pImpl->AttachShader();};
void Impl::AttachShader(){glAttachShader(mProgram, mVertexShader->mName);};
Дополнительная плюшка — можно избавиться от платформоспецифичности класса-обертки — он может вообще не знать о специфичных для реализации полях.
Скорее glAttachShader должен быть скрыт, ведь это деталь реализации. Более того, в gl шейдер это идентификатор, по этому странно что у него может быть какая-то разная реализация.
Можешь посмотреть мой подход gist.github.com/938592 (внимание, код не для продакшена и не вылезанный). Идея в том, что для разных материалов мы НЕ пишем c++ код, а пишем шэйдеры, а в json файлике прописываем атрибуты и юниформы.
Можешь посмотреть мой подход gist.github.com/938592 (внимание, код не для продакшена и не вылезанный). Идея в том, что для разных материалов мы НЕ пишем c++ код, а пишем шэйдеры, а в json файлике прописываем атрибуты и юниформы.
Да Pimpl очень удобен, особенно при разработке библитек. Это по факту самый безболезненный путь поддерживать совместимость ABI.
Странно что очень мало людей о нем знают.
Странно что очень мало людей о нем знают.
А ещё это способ делать неявно разделяемые данные, но это надо d указатель сильно пилить. А так надо помнить, что pimpl класс теряет возможность быть нормально скопированным без дополнительных телодвижений.
Чем Pimpl лучше нижеприведённого кода?
Имо, Pimpl имеет существенный недостаток — при добавлении или изменении метода приходится менять код аж в 4-х местах: в двух заголовочных файлах и в двух файлах реализации. Очевидно, это здорово ухудшает сопровождаемость кода.
К тому же классы с идиомой Pimpl изначально обладают семантикой указателей из-за того, что конструктор копирования по-умолчанию вместо копирования осуществляет присваивание указателей. Такое поведение несвойственно классам в C++, поэтому в классах с Pimpl приходится писать пользовательский конструктор копирования.
В статье также ни слова не сказано о том, что Pimpl может позволить избавиться от виртуальных функций. Странно, так как обычно это расценивают как преимущество.
/* SomeInterface.h */ class SomeInterface { public: static std::shared_ptr<SomeInterface> create(); virtual ~SomeInterface() { } virtual someMethod() = 0; }; /* SomeImplementation.h /* class SomeImplementation : public SomeInterface { public: SomeImplementation(); virtual someMethod(); }; /* SomeImplementation.cpp /* std::shared_ptr<SomeInterface> SomeInterface::create() { return std::make_shared<SomeImplementation>( ); }
Имо, Pimpl имеет существенный недостаток — при добавлении или изменении метода приходится менять код аж в 4-х местах: в двух заголовочных файлах и в двух файлах реализации. Очевидно, это здорово ухудшает сопровождаемость кода.
К тому же классы с идиомой Pimpl изначально обладают семантикой указателей из-за того, что конструктор копирования по-умолчанию вместо копирования осуществляет присваивание указателей. Такое поведение несвойственно классам в C++, поэтому в классах с Pimpl приходится писать пользовательский конструктор копирования.
В статье также ни слова не сказано о том, что Pimpl может позволить избавиться от виртуальных функций. Странно, так как обычно это расценивают как преимущество.
> Чем Pimpl лучше нижеприведённого кода?
Ок, вы создали интерфейс для своего класса, но в заголовочном файле вашего класса SomeImplementation всё равно будут присутствовать детали реализации. Идея Pimpl состоит в том, чтобы убрать из интерфейса детали реализации; добавьте в ваш SomeImplementation приватные методы и поля данных — и ваш подход не даст тех плюсов, которые дает Pimpl — см. «Преимущества идиомы Pimpl».
> при добавлении или изменении метода приходится менять код аж в 4-х местах
На самом деле нет. Видимо, вы не до конца поняли, как применять Pimpl. Если вы добавляете приватный метод — вы добавляете его только в приватный класс. Если вы добавляете публичный метод — вы добавляете его только в основной класс.
> в классах с Pimpl приходится писать пользовательский конструктор копирования
Верно. Товарищ комментарием выше уже обратил на это внимание. Но тут есть и обратная сторона медали, см. про Copy-Swap.
> В статье также ни слова не сказано о том, что Pimpl может позволить избавиться от виртуальных функций
Вроде сказано, в пункте «Расширенные способы композиции»
Ок, вы создали интерфейс для своего класса, но в заголовочном файле вашего класса SomeImplementation всё равно будут присутствовать детали реализации. Идея Pimpl состоит в том, чтобы убрать из интерфейса детали реализации; добавьте в ваш SomeImplementation приватные методы и поля данных — и ваш подход не даст тех плюсов, которые дает Pimpl — см. «Преимущества идиомы Pimpl».
> при добавлении или изменении метода приходится менять код аж в 4-х местах
На самом деле нет. Видимо, вы не до конца поняли, как применять Pimpl. Если вы добавляете приватный метод — вы добавляете его только в приватный класс. Если вы добавляете публичный метод — вы добавляете его только в основной класс.
> в классах с Pimpl приходится писать пользовательский конструктор копирования
Верно. Товарищ комментарием выше уже обратил на это внимание. Но тут есть и обратная сторона медали, см. про Copy-Swap.
> В статье также ни слова не сказано о том, что Pimpl может позволить избавиться от виртуальных функций
Вроде сказано, в пункте «Расширенные способы композиции»
> в заголовочном файле вашего класса SomeImplementation всё равно будут присутствовать детали реализации.
Смысл интерфейса именно в том и заключается, что только он доступен клиенту. В примере, приведенном Lokken, Вам (как клиенту) будет только доступен SomeInterface.h; здесь нет деталей реализации: есть только интерфейс и фабрика для создания/получения объекта, его реализующего.
Смысл интерфейса именно в том и заключается, что только он доступен клиенту. В примере, приведенном Lokken, Вам (как клиенту) будет только доступен SomeInterface.h; здесь нет деталей реализации: есть только интерфейс и фабрика для создания/получения объекта, его реализующего.
Фабрика, которая возвращает указатель на счетчике ссылок на реализацию.
Общее с pimpl — с устоявшимся интерфейсом не нужна перекомпиляция, что особенно важно, если класс лежит в dll. Цена изменения интерфейса и там и там одна и та же — при изменении базового интерфейса нужно будет точно так же менять три файла и пересобирать все, что их использует. При изменении реализации все дешево и там и там. Это общее, и пример уважаемого Lokken поэтому не понятен.
Различия. Pimpl, в отличие от com-подобной концепции, если определить соответствующие конструкторы, позволяет использовать себя, как обыкновенный с++ класс: можно определить несколько конструкторов, можно писать Foo foo, а не shared_ptr (и иметь raii без указателя на счетчике ссылок), передавать по значению (если спрятали виртуальность и наследуем только impl часть) и вообще вести себя для клиента так, как будто это обыкновенный с++ класс.
Общее с pimpl — с устоявшимся интерфейсом не нужна перекомпиляция, что особенно важно, если класс лежит в dll. Цена изменения интерфейса и там и там одна и та же — при изменении базового интерфейса нужно будет точно так же менять три файла и пересобирать все, что их использует. При изменении реализации все дешево и там и там. Это общее, и пример уважаемого Lokken поэтому не понятен.
Различия. Pimpl, в отличие от com-подобной концепции, если определить соответствующие конструкторы, позволяет использовать себя, как обыкновенный с++ класс: можно определить несколько конструкторов, можно писать Foo foo, а не shared_ptr (и иметь raii без указателя на счетчике ссылок), передавать по значению (если спрятали виртуальность и наследуем только impl часть) и вообще вести себя для клиента так, как будто это обыкновенный с++ класс.
Абсолютно верно. Да и виртуальные функции не добавят скорости, Pimpl в этом плане лучше. Я только понял, что имел ввиду Lokken — сбила с толку строка /* SomeImplementation.cpp /* — я подумал, что клиенту предоставляется также SomeImplementation, а он на самом деле используется только внутри someinterface.cpp.
Sign up to leave a comment.
Перевод статьи «Pimp My Pimpl», часть 1