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

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

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

C ++ -& CRTP. Тип стирание против полиморфизма

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

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

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

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

#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);
}

17

Решение

Полиморфизм виртуальных методов в стиле C ++:

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

Стирание типов на основе шаблонов в стиле C ++ (при этом стирание происходит с помощью полиморфизма виртуальных методов):

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

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

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

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

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

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

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

7

Другие решения

Вот одна точка зрения: вопрос, кажется, задает вопрос о том, как выбирать между поздним связыванием («полиморфизм во время выполнения») и ранним связыванием («полиморфизм во время компиляции»).

Как отмечает KerrekSB в своих комментариях, есть некоторые вещи, которые вы можете сделать с поздним связыванием, но с ранним связыванием это просто нереально. Многие применения шаблона «Стратегия» (декодирование сетевого ввода-вывода) или шаблона «Абстрактная фабрика» (фабрики классов, выбранные во время выполнения) попадают в эту категорию.

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

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

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

Другим компромиссом является размер программы. По крайней мере, в C ++ при использовании «полиморфизма во время компиляции» иногда всплывает двоичный размер, поскольку компилятор создает, оптимизирует и выдает различный код для каждой используемой специализации. Напротив, при позднем связывании существует только один путь кода.

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

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

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

5

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

0
По вопросам рекламы [email protected]