Как стать автором
Обновить

Комментарии 19

Мне понравилось, как pImpl позволяет скрыть реализацию.
К примеру, взять класс таймера, с разной реализацей для windows и linux

Можно конечно сделать так:
xTimer* timer = xTimer::Create();

Но с pImpl'ом интереснее:

glAttachShader(pImpl->mProgram, pImpl->mVertexShader->pImpl->mName);
ой, отправилось как-то o_O предыдущий комментарий читать без последней строчки

c pImpl'ом интереснее
xTimer* timer = new xTimer()

Но когда я у себя в коде написал
glAttachShader(pImpl->mProgram, pImpl->mVertexShader->pImpl->mName);
задумался, наверное что-то не так…

Как-то это можно красиво разрулить?

Реализация gpu-программы использует вершинный шейдер и должна знать о его реализации.
Как вариант, можно попробовать перегружать оператор -> и возвращать в нем pImpl. Тогда вроде бы получится так:
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 реализации например указатель на интерфейс.

Обращение внутри реализации и только.

Внутри реализации не нужно было бы писать pimpl->mProgram, это же сам impl и есть, а значит нужно писать просто mProgram или там this->mProgram.
Если под реализацией вы понимаете .cpp файл класса-обертки, то в нем нужно переадресовывать все в реализацию, а не просто использовать Impl-класс как хранилище приватных полей. То есть так:
void Class::AttachShader(){pImpl->AttachShader();};
void Impl::AttachShader(){glAttachShader(mProgram, mVertexShader->mName);};
Дополнительная плюшка — можно избавиться от платформоспецифичности класса-обертки — он может вообще не знать о специфичных для реализации полях.
Согласен, просто дело в том, что у меня часть полей и методов вне pImpl'а, грубо говоря, класс Texture2D в любой реализации будет иметь width и height и какой-то общий функционал. Поэтому их я оставляю в самом классе. Отсюда и вот эти pImpl->. Так-то да, специфичные вещи в сам pImpl выношу.
Скорее glAttachShader должен быть скрыт, ведь это деталь реализации. Более того, в gl шейдер это идентификатор, по этому странно что у него может быть какая-то разная реализация.
Можешь посмотреть мой подход gist.github.com/938592 (внимание, код не для продакшена и не вылезанный). Идея в том, что для разных материалов мы НЕ пишем c++ код, а пишем шэйдеры, а в json файлике прописываем атрибуты и юниформы.
Да, к pimpl это не имеет отношения, но зато наружу не торчит opengl (и главное glew.h).
Я и пользовался pImpl'ом, желая скрыть gl.h, d3d10.h, хидеры зависимостей. С этой задачей он справляется замечательно :)
В таких случаях лучше воспользоваться наследованием или просто интерфейс выделить.
Да Pimpl очень удобен, особенно при разработке библитек. Это по факту самый безболезненный путь поддерживать совместимость ABI.
Странно что очень мало людей о нем знают.
А ещё это способ делать неявно разделяемые данные, но это надо d указатель сильно пилить. А так надо помнить, что 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 всё равно будут присутствовать детали реализации.

Смысл интерфейса именно в том и заключается, что только он доступен клиенту. В примере, приведенном Lokken, Вам (как клиенту) будет только доступен SomeInterface.h; здесь нет деталей реализации: есть только интерфейс и фабрика для создания/получения объекта, его реализующего.
Фабрика, которая возвращает указатель на счетчике ссылок на реализацию.
Общее с pimpl — с устоявшимся интерфейсом не нужна перекомпиляция, что особенно важно, если класс лежит в dll. Цена изменения интерфейса и там и там одна и та же — при изменении базового интерфейса нужно будет точно так же менять три файла и пересобирать все, что их использует. При изменении реализации все дешево и там и там. Это общее, и пример уважаемого Lokken поэтому не понятен.
Различия. Pimpl, в отличие от com-подобной концепции, если определить соответствующие конструкторы, позволяет использовать себя, как обыкновенный с++ класс: можно определить несколько конструкторов, можно писать Foo foo, а не shared_ptr (и иметь raii без указателя на счетчике ссылок), передавать по значению (если спрятали виртуальность и наследуем только impl часть) и вообще вести себя для клиента так, как будто это обыкновенный с++ класс.
Абсолютно верно. Да и виртуальные функции не добавят скорости, Pimpl в этом плане лучше. Я только понял, что имел ввиду Lokken — сбила с толку строка /* SomeImplementation.cpp /* — я подумал, что клиенту предоставляется также SomeImplementation, а он на самом деле используется только внутри someinterface.cpp.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации