Pimpl идиома против Pure интерфейс виртуального класса - PullRequest
114 голосов
/ 05 мая 2009

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

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

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

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

Ответы [ 10 ]

59 голосов
/ 05 мая 2009

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

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

    Копирование по значению, личность никогда не важна. Для него уместно быть ключом в std :: map. Например, класс "string" или класс "date" или класс "complex number". «Копировать» экземпляры такого класса имеет смысл.

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

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

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

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

33 голосов
/ 05 мая 2009

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

27 голосов
/ 05 мая 2009

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

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

14 голосов
/ 25 февраля 2010

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

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

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

  1. когда прокси-вызовы встроены, при наследовании обязательно потребуется дополнительный доступ к объекту VTABLE во время выполнения
  2. объем занимаемой памяти в классе pimpl public-proxy меньше (вы можете легко оптимизировать для более быстрой замены и других подобных оптимизаций)
8 голосов
/ 05 октября 2010

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

8 голосов
/ 05 мая 2009

Существует очень реальная проблема с совместно используемыми библиотеками, которую идиома 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.

На стороне пользователя кода 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())
{}

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

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

6 голосов
/ 27 февраля 2010

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

3 голосов
/ 13 июня 2017

Хотя в других ответах это широко освещено, возможно, я могу быть более откровенным об одном преимуществе 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 голосов
/ 18 января 2018

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

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

2 голосов
/ 05 мая 2009

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

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

Яблоки и апельсины действительно.

...