Pimpl idiom vs Чистый интерфейс виртуального класса

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

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

Виртуальный класс Pure с другой стороны имеет скрытую косвенность (vtable) для наследующей реализации, и я понимаю, что никаких накладных расходов нет.
EDIT. Но вам понадобится factory, если вы создадите объект извне

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

Ответ 1

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

  • Тип значения

    Скопировать по значению, идентификация никогда не важна. Он подходит для того, чтобы быть ключом в std:: map. Например, класс "строка" или класс "дата" или класс "сложный номер". Для "копирования" экземпляров такого класса имеет смысл.

  • Тип объекта

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

Оба pImpl и чистый абстрактный базовый класс - это методы сокращения временных зависимостей компиляции.

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

Ответ 2

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

Ответ 3

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

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

Ответ 4

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

  • Они более прямые (это субъективное мнение). Идиома Pimpl заставляет меня чувствовать, что я пишу код "для компилятора", а не для "следующего разработчика", который будет читать мой код.
  • Некоторые рамки тестирования имеют прямую поддержку для Mocking чистых виртуальных классов
  • Это правда, что вам нужно a factory быть доступным извне. Но если вы хотите использовать полиморфизм: это также "pro", а не "con".... и простой метод factory на самом деле не так больно

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

  • когда прокси-вызовы встроены, а наследование обязательно требует дополнительного доступа к объекту VTABLE во время выполнения
  • объем памяти pimpl public-proxy-class меньше (вы можете легко оптимизировать более быстрые свопы и другие подобные оптимизации)

Ответ 5

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

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

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

Ответ 6

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

Ответ 7

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

Ответ 8

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

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

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

Ответ 9

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

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

// 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();