Подтвердить что ты не робот

Наследование и виртуальные функции Vs Generic Programming

Мне нужно понять, что действительно ли Inheritance & virtual functions не нужно в С++, и можно добиться всего, используя Generic programming. Это произошло из Alexander Stepanov, и лекция, которую я наблюдал, была Александр Степанов: STL и его принципы дизайна

4b9b3361

Ответ 1

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

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

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

Наследование - это сделать абстрактную концепцию более конкретной, добавив подробности. Общее программирование - это, по сути, генерация кода.

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

// internal helper base
class TEBase { /* ... */ };

// internal helper derived TEMPLATE class (unbounded family!)
template <typename T> class TEImpl : public TEBase { /* ... */ }

// single public interface class
class TE
{
  TEBase * impl;
public:
  // "infinitely many" constructors:
  template <typename T> TE(const T & x) : impl(new TEImpl<T>(x)) { }
  // ...
};

Ответ 2

Они служат для разных целей. Общее программирование (по крайней мере, на С++) - это время компиляции polyorphisim и виртуальные функции о полиморфизме во время выполнения.

Если выбор конкретного типа зависит от пользовательского ввода, вам действительно нужно использовать полиморфизм - шаблоны вам не помогут.

Ответ 3

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

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

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

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

Ответ 4

Похоже, это очень академический вопрос, как и в большинстве случаев в жизни, есть много способов сделать что-то, и в случае С++ у вас есть несколько способов решить вещи. Нет необходимости иметь отношение XOR к вещам.

Ответ 5

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

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

Я сделал некоторое тяжелое обобщенное программирование, использующее статический полиморфизм для реализации общей библиотеки RPC (https://github.com/bytemaster/mace (ветвь rpc_static_poly)). В этом случае протокол (JSON-RPC, транспорт (TCP/UDP/Stream/etc) и типы) известны во время компиляции, поэтому нет причин для отправки vtable... или есть?

Когда я запускаю код через предварительный процессор для single.cpp, он выводит 250 000 строк и занимает 30 + секунд для компиляции одного объектного файла. Я реализовал "идентичную" функциональность в Java и С#, и она скомпилируется примерно через секунду.

Почти каждый заголовок stl или boost, который вы добавляете, добавляет тысячи или 10 тысяч строк кода, которые должны обрабатываться для каждого объекта файла, большая часть которого избыточна.

Знаете ли вы время компиляции? В большинстве случаев они оказывают более существенное влияние на конечный продукт, чем "максимально оптимизированное удаление vtable". Причина в том, что каждый "ошибка" требует цикла "попробовать исправить, скомпилировать, протестировать", и если каждый цикл займет 30 + секунд, развитие замедлит сканирование (обратите внимание, что мотивация для Google переходит на язык).

Проведя несколько дней с java и С#, я решил, что мне нужно "передумать" мой подход к С++. Нет причин, по которым программа на С++ должна компилироваться намного медленнее, чем базовый C, который будет реализовывать ту же функцию.

Теперь я выбираю полиморфизм во время выполнения, если профилирование не показывает, что узкое место находится в диспетчерах vtable. Теперь я использую шаблоны, чтобы обеспечить "точно в срок" полиморфизм и тип безопасный интерфейс поверх базового объекта, который имеет дело с "void *" или абстрактным базовым классом. Таким образом, пользователям не нужно выводить из моих "интерфейсов" и по-прежнему иметь "ощущение" общего программирования, но они получают выгоду от быстрого времени компиляции. Если производительность становится проблемой, то общий код может быть заменен статическим полиморфизмом.

Результаты драматичны, время компиляции сократилось с 30 секунд до секунды. Исходный код пост-препроцессора теперь составляет несколько тысяч строк вместо 250 000 строк.

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