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



Мне было интересно, что заставит программиста выбрать либо идиому Pimpl, либо чистый ВИРТУАЛЬНЫЙ КЛАСС и наследование.



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



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



Что делает чистый ВИРТУАЛЬНЫЙ КЛАСС менее желательным, чем идиома pimpl?

533   10  

10 ответов:

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

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

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

  2. объект типа

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

по интеллекту 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())
{}

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

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

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

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

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

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

яблоки и апельсины на самом деле.

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

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

особенно "время сборки" - это проблема, которую вы можете решить с помощью лучшего оборудования или с помощью таких инструментов, как Incredibuild ( www.incredibuild.com ), таким образом, нет влияние на разработку программного обеспечения. Проектирование программного обеспечения должно быть в целом независимым от способа построения программного обеспечения.

Comments

    Ничего не найдено.