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

StackOverflow https://stackoverflow.com/questions/825018

  •  05-07-2019
  •  | 
  •  

Вопрос

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

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

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

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

Это было полезно?

Решение

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

<Ол>
  • Тип значения

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

  • Тип сущности

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

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

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

    Другие советы

    Указатель на реализацию обычно скрывает детали структурной реализации. Интерфейсы предназначены для реализации различных реализаций. Они действительно служат двум различным целям.

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

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

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

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

    <Ол>
  • когда прокси-вызовы встроены, при наследовании обязательно потребуется дополнительный доступ к объекту VTABLE во время выполнения
  • объем занимаемой памяти в классе 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)
    {}
    

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

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

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

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

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

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

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

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

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

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

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

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

    Лицензировано под: CC-BY-SA с атрибуция
    Не связан с StackOverflow
    scroll top