Идиома Pimpl против интерфейса виртуального класса Pure

118

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

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

С другой стороны, виртуальный класс Pure имеет неявную косвенную адресацию (vtable) для наследующей реализации, и я понимаю, что никаких накладных расходов на создание объекта нет.
РЕДАКТИРОВАТЬ : Но вам понадобится фабрика, если вы создадите объект снаружи

Что делает чистый виртуальный класс менее желательным, чем идиома pimpl?

Аркаитц Хименес
источник
3
Отличный вопрос, просто хотел задать то же самое. См. Также boost.org/doc/libs/1_41_0/libs/smart_ptr/sp_techniques.html
Фрэнк

Ответы:

60

При написании класса C ++ уместно подумать, будет ли он

  1. Тип значения

    Копирование по ценности, идентичность никогда не важна. Это уместно, чтобы он был ключом в std :: map. Пример, класс «строка», класс «дата» или класс «комплексное число». Имеет смысл «копировать» экземпляры такого класса.

  2. Тип сущности

    Идентичность важна. Всегда передается по ссылке, а не по «значению». Часто вообще не имеет смысла «копировать» экземпляры класса. Когда это действительно имеет смысл, обычно более уместен метод полиморфного «клонирования». Примеры: класс Socket, класс базы данных, класс «политики», все, что было бы «закрытием» на функциональном языке.

Как pImpl, так и чистый абстрактный базовый класс - это методы уменьшения зависимостей времени компиляции.

Однако я всегда использую pImpl только для реализации типов значений (тип 1), и только иногда, когда я действительно хочу свести к минимуму взаимосвязь и зависимости во время компиляции. Часто это не стоит того. Как вы правильно заметили, синтаксических издержек больше, потому что вам нужно писать методы пересылки для всех общедоступных методов. Для классов типа 2 я всегда использую чистый абстрактный базовый класс со связанным заводским методом (ами).

Пол Холлингсворт
источник
6
См. Комментарий Поля де Вриза к этому ответу . Pimpl и Pure Virtual значительно отличаются, если вы работаете в библиотеке и хотите поменять местами свои .so / .dll без пересборки клиента. Клиенты ссылаются на интерфейсы pimpl по имени, поэтому достаточно сохранить старые сигнатуры методов. OTOH в чисто абстрактном случае они эффективно связываются с помощью индекса vtable, поэтому методы переупорядочения или вставка в середину нарушат совместимость.
SnakE
1
Вы можете только добавлять (или переупорядочивать) методы во внешнем интерфейсе класса Pimpl, чтобы поддерживать двоичную сопоставимость. По логике вещей, вы все же изменили интерфейс, и он кажется немного хитрым. Ответ здесь - разумный баланс, который также может помочь при модульном тестировании посредством «внедрения зависимостей»; но ответ всегда зависит от требований. Сторонние разработчики библиотеки (в отличие от использования библиотеки в вашей собственной организации) могут в значительной степени предпочесть Pimpl.
Spacen Jasset
31

Pointer to implementationобычно заключается в сокрытии деталей структурной реализации. Interfacesкасаются создания различных реализаций. Они действительно служат двум разным целям.

D.Shawley
источник
13
не обязательно, я видел классы, которые хранят несколько pimpls в зависимости от желаемой реализации. Часто это говорят, что win32 impl против linux impl чего-то, что должно быть реализовано по-разному для каждой платформы.
Дуг Т.
14
Но вы можете использовать интерфейс, чтобы отделить детали реализации и скрыть их
Аркаитц Хименес
6
Хотя вы можете реализовать pimpl с помощью интерфейса, часто нет причин разделять детали реализации. Так что нет причин становиться полиморфным. Причина для Pimpl, чтобы сохранить детали реализации от клиента (в C ++ , чтобы держать их из заголовка). Вы можете сделать это, используя абстрактную базу / интерфейс, но обычно это излишне.
Майкл Берр,
10
Почему это перебор? Я имею в виду, что он медленнее, чем метод интерфейса pimpl? Могут быть логические причины, но с практической точки зрения я бы сказал, что проще сделать это с абстрактным интерфейсом
Аркаитц Хименес
1
Я бы сказал, что абстрактный базовый класс / интерфейс - это «нормальный» способ делать что-то и упрощает тестирование с помощью
имитации
28

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

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

Pontus Gagge
источник
17

Я искал ответ на тот же вопрос. После прочтения некоторых статей и некоторой практики я предпочитаю использовать «интерфейсы чистого виртуального класса» .

  1. Они более прямолинейны (это субъективное мнение). Идиома Pimpl заставляет меня чувствовать, что я пишу код «для компилятора», а не для «следующего разработчика», который прочитает мой код.
  2. Некоторые среды тестирования имеют прямую поддержку Mocking чистых виртуальных классов.
  3. Это правда, что вам нужно, чтобы фабрика была доступна извне. Но если вы хотите использовать полиморфизм: это тоже «за», а не «против». ... и простой заводской метод не особо вредит

Единственный недостаток ( я пытаюсь разобраться в этом ) заключается в том, что идиома pimpl может быть быстрее

  1. когда прокси-вызовы встроены, при наследовании обязательно требуется дополнительный доступ к объекту VTABLE во время выполнения
  2. объем памяти, занимаемый публичным прокси-классом pimpl, меньше (вы можете легко выполнять оптимизацию для более быстрой подкачки и других подобных оптимизаций)
Илиас Бартолини
источник
21
Также помните, что, используя наследование, вы вводите зависимость от макета vtable. Чтобы поддерживать ABI, вы больше не можете изменять виртуальные функции (добавление в конце в некотором роде безопасно, если нет дочерних классов, которые добавляют собственные виртуальные методы).
Поль де Вриз
1
^ Этот комментарий здесь должен быть липким.
CodeAngry
10

Ненавижу прыщи! Они делают класс некрасивым и нечитабельным. Все методы перенаправлены на прыщ. Вы никогда не увидите в заголовках, какие функции имеет класс, поэтому вы не можете его реорганизовать (например, просто изменить видимость метода). Класс чувствует себя «беременным». Я думаю, что использование iterfaces лучше и действительно достаточно, чтобы скрыть реализацию от клиента. Вы можете позволить одному классу реализовать несколько интерфейсов, чтобы они оставались тонкими. Отдавать предпочтение интерфейсам! Примечание: вам не нужен фабричный класс. Релевантно то, что клиенты класса общаются с его экземплярами через соответствующий интерфейс. Скрытие частных методов я считаю странной паранойей и не вижу для этого причин, поскольку у нас есть интерфейсы.

Маша Ананьева
источник
1
Бывают случаи, когда нельзя использовать чисто виртуальные интерфейсы. Например, если у вас есть устаревший код и два модуля, которые вам нужно разделить, не касаясь их.
AlexTheo 06
Как отметил @Paul de Vrieze ниже, вы теряете совместимость с ABI при изменении методов базового класса, потому что у вас есть неявная зависимость от vtable класса. Это зависит от варианта использования, является ли это проблемой.
Х.
«Скрытие частных методов я считаю странной паранойей». Разве это не позволяет вам скрыть зависимости и, следовательно, минимизировать время компиляции при изменении зависимости?
pooya13
Я также не понимаю, почему фабрики легче переработать, чем pImpl. Не выходите ли вы в обоих случаях из «интерфейса» и не меняете реализацию? В Factory вам нужно изменить один файл .h и один .cpp, а в pImpl вам нужно изменить один .h и два файла .cpp, но это все, и вам обычно не нужно изменять файл cpp интерфейса pImpl.
pooya13
8

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

Чтобы подробно объяснить проблему, рассмотрите следующий код в вашей общей библиотеке / заголовке:

// header
struct A
{
public:
  A();
  // more public interface, some of which uses the int below
private:
  int a;
};

// library 
A::A()
  : a(0)
{}

Компилятор генерирует код в разделяемой библиотеке, который вычисляет адрес целого числа, которое должно быть инициализировано, как определенное смещение (в данном случае, вероятно, ноль, потому что это единственный член) от указателя на объект A, который, как ему известно, является this.

На стороне пользователя кода a new Aсначала выделяет sizeof(A)байты памяти, а затем передает указатель на эту память A::A()конструктору как this.

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

С помощью pimpl'ing вы можете безопасно добавлять и удалять элементы данных во внутренний класс, поскольку выделение памяти и вызов конструктора происходят в общей библиотеке:

// header
struct A
{
public:
  A();
  // more public interface, all of which delegates to the impl
private:
  void * impl;
};

// library 
A::A()
  : impl(new A_impl())
{}

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

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


источник
4
Вместо void * я думаю, что более традиционным является пересылка объявления реализующего класса:class A_impl *impl_;
Франк Крюгер,
9
Я не понимаю, вам не следует объявлять частные члены в виртуальном чистом классе, который вы собираетесь использовать в качестве интерфейса, идея состоит в том, чтобы класс оставался чисто абстрактным, без размера, только чистые виртуальные методы, я ничего не вижу нельзя делать через общие библиотеки
Аркаитц Хименес
@Frank Krueger: Ты прав, я ленился. @Arkaitz Jimenez: Небольшое недоразумение; если у вас есть класс, который содержит только чистые виртуальные функции, тогда нет особого смысла говорить об общих библиотеках. С другой стороны, если вы имеете дело с разделяемыми библиотеками, упрощение публичных классов может быть разумным по причине, изложенной выше.
10
Это просто неверно. Оба метода позволяют скрыть состояние реализации ваших классов, если вы сделаете другой класс «чисто абстрактным базовым» классом.
Пол Холлингсворт,
10
Первое предложение в вашем anser подразумевает, что чистые виртуалы со связанным фабричным методом каким-то образом не позволяют вам скрыть внутреннее состояние класса. Это не правда. Оба метода позволяют скрыть внутреннее состояние класса. Разница в том, как это выглядит для пользователя. pImpl позволяет вам по-прежнему представлять класс с семантикой значений, но также скрывать внутреннее состояние. Чистый абстрактный базовый класс + фабричный метод позволяет вам представлять типы сущностей, а также позволяет скрыть внутреннее состояние. Именно так работает COM. В главе 1 "Essential COM" это обсуждается.
Пол Холлингсворт,
6

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

Сэм
источник
3

Хотя это широко освещено в других ответах, возможно, я могу более подробно рассказать об одном преимуществе pimpl по сравнению с виртуальными базовыми классами:

Подход pimpl прозрачен с точки зрения пользователя, то есть вы можете, например, создавать объекты класса в стеке и использовать их непосредственно в контейнерах. Если вы попытаетесь скрыть реализацию с помощью абстрактного виртуального базового класса, вам нужно будет вернуть общий указатель на базовый класс из фабрики, что усложнит его использование. Рассмотрим следующий эквивалентный клиентский код:

// Pimpl
Object pi_obj(10);
std::cout << pi_obj.SomeFun1();

std::vector<Object> objs;
objs.emplace_back(3);
objs.emplace_back(4);
objs.emplace_back(5);
for (auto& o : objs)
    std::cout << o.SomeFun1();

// Abstract Base Class
auto abc_obj = ObjectABC::CreateObject(20);
std::cout << abc_obj->SomeFun1();

std::vector<std::shared_ptr<ObjectABC>> objs2;
objs2.push_back(ObjectABC::CreateObject(13));
objs2.push_back(ObjectABC::CreateObject(14));
objs2.push_back(ObjectABC::CreateObject(15));
for (auto& o : objs2)
    std::cout << o->SomeFun1();
Суперфлай Джон
источник
2

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

Цель виртуальных классов больше связана с разрешением полиморфизма, т.е. у вас есть неизвестный указатель на объект производного типа, и когда вы вызываете функцию x, вы всегда получаете правильную функцию для любого класса, на который фактически указывает базовый указатель.

Действительно, яблоки и апельсины.

Роберт С. Барнс
источник
Я согласен с яблоками / апельсинами. Но похоже, что вы используете pImpl для функционала. Моя цель - в основном сборочно-техническая и информационная.
xtofl
2

Самая неприятная проблема, связанная с идиомой pimpl, заключается в том, что она чрезвычайно затрудняет поддержку и анализ существующего кода. Таким образом, используя pimpl, вы платите временем и разочарованием разработчика только для того, чтобы «уменьшить зависимости и время сборки и минимизировать раскрытие заголовков деталей реализации». Решайте сами, действительно ли оно того стоит.

В частности, проблема «времени сборки» - это проблема, которую вы можете решить с помощью более совершенного оборудования или таких инструментов, как Incredibuild (www.incredibuild.com, также уже включенный в Visual Studio 2017), таким образом не влияя на дизайн вашего программного обеспечения. Дизайн программного обеспечения, как правило, не должен зависеть от способа его создания.

Trantor
источник
Вы также платите за время разработчика, когда время сборки составляет 20 минут вместо 2, так что это немного баланс, реальная модульная система здесь очень поможет.
Arkaitz Jimenez
IMHO, способ построения программного обеспечения не должен вообще влиять на внутренний дизайн. Это совсем другая проблема.
Trantor
2
Что затрудняет анализ? Куча вызовов в пересылке файла реализации в класс Impl не звучит сложно.
mabraham
2
Представьте себе реализации отладки, в которых используются и pimpl, и интерфейсы. Начиная с вызова в пользовательском коде A, вы отслеживаете интерфейс B, переходите к прыщавому классу C, чтобы наконец начать отладку класса реализации D ... Четыре шага, пока вы не сможете проанализировать, что на самом деле происходит. И если все это реализовано в DLL, вы, возможно, найдете интерфейс C где-то посередине ...
Trantor
Зачем использовать интерфейс с pImpl, если pImpl также может выполнять работу интерфейса? (то есть это может помочь вам добиться инверсии зависимостей)
pooya13