Методы С++: тип-стирание против чистого полиморфизма - программирование
Подтвердить что ты не робот

Методы С++: тип-стирание против чистого полиморфизма

Каковы преимущества/недостатки двух методов в сравнении? И что еще более важно: зачем и когда нужно использовать друг друга? Это только вопрос личного вкуса/предпочтения?

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

С++ - & CRTP. Тип стирания и полиморфизм

Пожалуйста, обратите внимание, что я очень хорошо понимаю обе методики. С этой целью я предоставляю простой, самостоятельный, рабочий пример ниже, который я с удовольствием удаляю, если это кажется ненужным. Однако на этом примере следует уточнить, что означают два метода в отношении моего вопроса. Мне не интересно обсуждать номенклатуры. Кроме того, я знаю разницу между полиморфизмом компиляции и времени выполнения, хотя я бы не счел это актуальным для вопроса. Обратите внимание, что мой интерес меньше в показателях производительности, если они есть. Однако, если бы был какой-то яркий аргумент для того или другого, основанного на производительности, мне было бы любопытно его прочитать. В частности, я хотел бы услышать о конкретных примерах (без кода), которые действительно будут работать только с одним из двух подходов.

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

Код, приведенный ниже, был протестирован (скомпилирован и запущен) с MS VisualStudio 2008, просто поместив все следующие кодовые блоки в один исходный файл. Он также должен компилироваться с помощью gcc в Linux, или я надеюсь/предполагаю, потому что не вижу причин, почему не (?):-) Я разделил/разделил код здесь для ясности.

Эти заголовочные файлы должны быть достаточными, right (?).

#include <iostream>
#include <vector>
#include <string>

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

class RefCount
{
  RefCount( const RefCount& );
  RefCount& operator= ( const RefCount& );
  int m_refCount;

  public:
    RefCount() : m_refCount(1) {}
    void Increment() { ++m_refCount; }
    int Decrement() { return --m_refCount; }
};

Это простой пример/пример стирания стирания. Он был скопирован и частично изменен из следующей статьи. В основном я старался сделать это максимально ясно и ясно. http://www.cplusplus.com/articles/oz18T05o/

class Object {
  struct ObjectInterface {
    virtual ~ObjectInterface() {}
    virtual std::string GetSomeText() const = 0;
  };

  template< typename T > struct ObjectModel : ObjectInterface {
    ObjectModel( const T& t ) : m_object( t ) {}
    virtual ~ObjectModel() {}
    virtual std::string GetSomeText() const { return m_object.GetSomeText(); }
    T m_object;
 };

  void DecrementRefCount() {
    if( mp_refCount->Decrement()==0 ) {
      delete mp_refCount; delete mp_objectInterface;
      mp_refCount = NULL; mp_objectInterface = NULL;
    }
  }

  Object& operator= ( const Object& );
  ObjectInterface *mp_objectInterface;
  RefCount *mp_refCount;

  public:
    template< typename T > Object( const T& obj )
      : mp_objectInterface( new ObjectModel<T>( obj ) ), mp_refCount( new RefCount ) {}
    ~Object() { DecrementRefCount(); }

    std::string GetSomeText() const { return mp_objectInterface->GetSomeText(); }

    Object( const Object &obj ) {
      obj.mp_refCount->Increment(); mp_refCount = obj.mp_refCount;
      mp_objectInterface = obj.mp_objectInterface;
    }
};

struct MyObject1 { std::string GetSomeText() const { return "MyObject1"; } };
struct MyObject2 { std::string GetSomeText() const { return "MyObject2"; } };

void UseTypeErasure() {
  typedef std::vector<Object> ObjVect;
  typedef ObjVect::const_iterator ObjVectIter;

  ObjVect objVect;
  objVect.push_back( Object( MyObject1() ) );
  objVect.push_back( Object( MyObject2() ) );

  for( ObjVectIter iter = objVect.begin(); iter != objVect.end(); ++iter )
    std::cout << iter->GetSomeText();
}

Насколько мне известно, это похоже на то, что используется полиморфизм, а может и не (?).

struct ObjectInterface {
  virtual ~ObjectInterface() {}
  virtual std::string GetSomeText() const = 0;
};

struct MyObject3 : public ObjectInterface {
  std::string GetSomeText() const { return "MyObject3"; } };

struct MyObject4 : public ObjectInterface {
  std::string GetSomeText() const { return "MyObject4"; } };

void UsePolymorphism() {
  typedef std::vector<ObjectInterface*> ObjVect;
  typedef ObjVect::const_iterator ObjVectIter;

  ObjVect objVect;
  objVect.push_back( new MyObject3 );
  objVect.push_back( new MyObject4 );

  for( ObjVectIter iter = objVect.begin(); iter != objVect.end(); ++iter )
    std::cout << (*iter)->GetSomeText();

  for( ObjVectIter iter = objVect.begin(); iter != objVect.end(); ++iter )
    delete *iter;
}

И, наконец, для тестирования всего вышеперечисленного.

int main() {
  UseTypeErasure();
  UsePolymorphism();
  return(0);
}
4b9b3361

Ответ 1

Полиморфизм на основе виртуального метода в стиле С++:

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

Стирание стилей на основе стиля стилей на С++ (с использованием полиморфизма на основе виртуального метода, выполняющего стирание):

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

Теперь, что лучше? Ну, это зависит от того, хороши или плохи эти вещи в вашей конкретной ситуации.

В качестве явного примера std::function<...> использует стирание типа, которое позволяет ему принимать указатели на функции, ссылки на функции, вывод целой кучи основанных на шаблонах функций, которые генерируют типы во время компиляции, myraids функторов, которые имеют оператор ( ) и лямбда. Все эти типы не связаны друг с другом. И поскольку они не привязаны к virtual operator(), когда они используются вне контекста std::function, абстракция, которую они представляют, может быть скомпилирована. Вы не могли бы сделать это без стирания типа, и вы, вероятно, не захотели бы.

С другой стороны, просто потому, что класс имеет метод под названием DoFoo, не означает, что все они делают одно и то же. При полиморфизме это не просто вызов DoFoo, а DoFoo из определенного интерфейса.

Что касается вашего образца кода... ваш GetSomeText должен быть virtual ... override в случае полиморфизма.

Нет необходимости ссылаться на счет только потому, что вы используете стирание типа. Нет необходимости не использовать подсчет ссылок только потому, что вы используете полиморфность.

Ваш Object может обернуть T* так же, как вы сохранили vector исходных указателей в другом случае с ручным уничтожением их содержимого (что эквивалентно вызову delete). Ваш Object может обернуть std::shared_ptr<T>, а в другом случае вы можете иметь vector из std::shared_ptr<T>. Ваш Object может содержать std::unique_ptr<T>, эквивалентный наличию вектора std::unique_ptr<T> в другом случае. Ваш Object ObjectModel может извлекать конструкторы копий и операторы присваивания из T и выставлять их на Object, что позволяет использовать полнофункциональную семантику значения для вашего Object, что соответствует a vector T в вашем случае полиморфизма.

Ответ 2

Здесь один взгляд: вопрос, кажется, спрашивает, как следует выбирать между поздним связыванием ( "полиморфизм времени выполнения" ) и ранним связыванием ( "полиморфизм времени компиляции" ).

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

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

Есть, по крайней мере, некоторые люди, которые считают, что шаблоны С++ в любой форме или форме невозможно понять. Или, возможно, есть еще одна, менее драматичная оговорка с шаблонами. Шаблоны С++ имеют много маленьких штрихов ( "когда мне нужно использовать ключевые слова" typename "и" template "?" ), А также неочевидные трюки (SFINAE приходит на ум).

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

Другим компромиссом является размер программы. В С++, по крайней мере, использование "полиморфизма времени компиляции" иногда порождает бинарный размер, поскольку компилятор создает, оптимизирует и испускает разный код для каждой используемой специализации. Напротив, при связывании поздно существует только один путь кода.

Интересно сравнить тот же компромисс, который делается в другом контексте. Возьмите веб-приложения, где вы используете (некоторый тип) полиморфизм для решения различий между браузерами и, возможно, для интернационализации (i18n)/локализации. Теперь, написанное вручную веб-приложение JavaScript, скорее всего, будет использовать то, что составляет последнее связывание здесь, путем использования методов, которые обнаруживают возможности во время выполнения, чтобы выяснить, что делать. Библиотеки, такие как jQuery, используют этот подход.

Другой подход - написать другой код для каждой возможной возможности браузера /i 18n. Хотя это звучит абсурдно, это далеко не неслыханно. Этот инструмент использует Google Web Toolkit. GWT имеет механизм "отложенного привязки", используемый для специализации выхода компилятора для разных браузеров и разных локализаций. Механизм "отложенной привязки" GWT использует раннее связывание: компилятор GWT Java-to-JavaScript определяет все возможные способы полиморфизма, которые могут потребоваться, и выдает совершенно разные "двоичные" для каждого.

Компромиссы аналогичны. Обертывание головы вокруг того, как вы расширяете GWT с использованием отложенного переплета, может быть головной болью и половиной; Наличие знаний во время компиляции позволяет компилятору GWT оптимизировать каждую специализацию отдельно, что, возможно, дает лучшую производительность и меньший размер для каждой специализации; Все приложение GWT может оказаться во много раз больше сопоставимого приложения jQuery из-за всех предварительно скомпилированных специализаций.

Ответ 3

Одно из преимуществ дженериков во время выполнения, о которых никто не упоминал (?), - это возможность для кода, который генерируется и вводится в запущенное приложение, использовать те же List, Hashmap / Dictionary и т.д., что все остальное в это приложение уже использует. Почему вы хотите это сделать, это еще один вопрос.