Почему С++ STL настолько сильно зависит от шаблонов? (а не на * интерфейсах *)

Я имею в виду, помимо его обязательного имени (Стандартная библиотека шаблонов)...

С++ изначально предназначался для представления концепций ООП на C. То есть: вы могли бы сказать, что конкретный объект мог и не мог сделать (независимо от того, как он это работает) на основе его иерархии классов и классов. Некоторые композиции способностей сложнее описать таким образом из-за проблемности множественного наследования и того факта, что С++ поддерживает концепцию интерфейсов несколько неуклюжим способом (по сравнению с java и т.д.), Но там (и может быть улучшенный).

И затем в игру вошли шаблоны вместе с STL. STL, казалось, принял классические концепции ООП и сбросил их вниз, используя вместо этого шаблоны.

Должно существовать различие между случаями, когда шаблоны используются для обобщения типов, где типы тем не имеют значения для работы шаблона (контейнеры, например). Наличие vector<int> имеет смысл.

Однако во многих других случаях (итераторы и алгоритмы) шаблонные типы должны следовать "концепции" (Input Iterator, Forward Iterator и т.д.), где фактические детали концепции полностью определяются реализацией функции/класса шаблона, а не классом типа, используемого с шаблоном, что несколько противоречит использованию ООП.

Например, вы можете указать функцию:

void MyFunc(ForwardIterator<...> *I);

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

ожидает Forward Iterator, только взглянув на его определение, где вам нужно либо посмотреть на реализацию, либо на документацию для:

template <typename Type> void MyFunc(Type *I);

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

Однако я ищу более глубокую причину отказа от классического ООП в пользу шаблонов для STL? (Предполагая, что вы читаете так далеко: P)

Ответ 1

Короткий ответ - "потому что С++ перешел". Да, еще в конце 70-х, Stroustrup намеревался создать обновленный C с возможностями OOP, но это уже давно. К тому времени, когда язык был стандартизован в 1998 году, он больше не был языком ООП. Это был язык с несколькими парадигмами. Это, безусловно, имело некоторую поддержку кода ООП, но у него также был завершенный язык формулировки шаблонов, он позволил метапрограммировать время компиляции, и люди обнаружили общее программирование. Внезапно ООП просто не казался таким важным. Не тогда, когда мы можем писать более простой, более сжатый и более эффективный код, используя методы, доступные через шаблоны и общее программирование.

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

Вот почему сообщество С++ сегодня гораздо больше интересуется общим программированием и почему все, наконец, начинают понимать, что функциональное программирование тоже довольно умно. ООП сам по себе просто не очень красив.

Попробуйте составить график зависимостей гипотетического STL. Сколько классов должны знать друг о друге? Было бы много зависимостей. Вы могли бы включить только заголовок vector, не получив при этом iterator или даже iostream? STL делает это легко. Вектор знает о типе итератора, который он определяет, и что все. Алгоритмы STL ничего не знают. Им даже не нужно включать заголовок итератора, даже если все они принимают итераторы в качестве параметров. Какой из них более модульный?

STL не может следовать правилам ООП, как это определяет Java, но не достигает ли он целей ООП? Разве он не обеспечивает повторного использования, низкой связи, модульности и инкапсуляции?

И не достигают ли эти цели лучше, чем версия с ООП-версией?

Что касается того, почему STL был принят на язык, произошло несколько событий, которые привели к STL.

Сначала в С++ были добавлены шаблоны. Они были добавлены по той же причине, что дженерики были добавлены в .NET. Казалось бы, неплохо было написать такие вещи, как "контейнеры типа Т", не отбрасывая безопасность типов. Конечно, реализация, на которой они остановились, была намного сложнее и мощнее.

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

Он представил его комитету по языку С++, которому потребовалось довольно много времени, чтобы привыкнуть к нему, потому что оно выглядело так странно и по-другому, но в конечном итоге осознало, что оно работает лучше, чем традиционные эквиваленты ООП, которые им пришлось бы включать в противном случае. Поэтому они внесли несколько корректировок и приняли его в стандартную библиотеку.

Это был не идеологический выбор, это был не политический выбор "мы хотим быть ООПом или нет", а очень прагматичным. Они оценили библиотеку и увидели, что она работает очень хорошо.

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

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

И STL имеет для работы с примитивными типами, поскольку примитивные типы - это все, что у вас есть на C, и они являются основной частью обоих языков. Если STL не работал с встроенными массивами, это было бы бесполезно.

В вашем вопросе есть сильное предположение, что ООП "лучше". Мне любопытно узнать, почему. Вы спрашиваете, почему они "отказались от классического ООП". Мне интересно, почему они должны были застрять в этом. Какие преимущества у него были бы?

Ответ 2

Самый прямой ответ на то, о чем я думаю, вы спрашиваете/жалуетесь, таков: предположение о том, что С++ является языком ООП, является ложным предположением.

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

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

Ответ 3

Я понимаю, что Stroustrup первоначально предпочитал дизайн контейнера в стиле OOP, и на самом деле не видел другого способа сделать это. Александр Степанов является ответственным за STL, а его цели не включают "сделать его объектно-ориентированным" :

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

STL, по крайней мере для меня, представляет собой единственный способ программирования. Это действительно отличается от программирования на С++, поскольку оно было представлено и представлено в большинстве учебников. Но, видите ли, я не пытался программировать на С++, я пытался найти правильный способ справиться с программным обеспечением....

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

(Он объясняет, почему наследование и виртуальные объекты - объектно-ориентированный дизайн a.k.a. "были в корне ошибочны и не должны использоваться" в оставшейся части интервью).

Как только Степанов представил свою библиотеку в Страуструп, Страуструп и другие пошли через усилия геркулеса, чтобы попасть в стандарт ISO С++ (то же интервью):

Поддержка Bjarne Stroustrup имела решающее значение. Бьярне действительно хотел STL в стандарте, и если Бьярне чего-то хочет, он это получит.... Он даже заставлял меня вносить изменения в STL, которые я никогда бы не сделал для кого-либо еще... он самый единомышленник, которого я знаю. Он все делает. Ему потребовалось некоторое время, чтобы понять, что такое STL, но когда он это сделал, он был готов проталкивать его. Он также внес свой вклад в STL, поддержав мнение о том, что более чем один способ программирования действителен - без каких-либо препятствий и шумихи в течение более десяти лет и сочетания гибкости, эффективности, перегрузки и безопасности типов в шаблоны, которые сделали STL возможным. Я хотел бы заявить, что Бьярн - выдающийся разработчик языков моего поколения.

Ответ 4

Ответ можно найти в этом интервью

Ответ 5

Почему чистый дизайн ООП для библиотеки данных и библиотеки алгоритмов будет лучше?! ООП не является решением для каждой вещи.

IMHO, STL - самая элегантная библиотека, которую я когда-либо видел:)

для вашего вопроса,

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

Вам нужна глупая вещь, как Бокс и Unboxing, чтобы скрыть неприятные предположения о так называемых языках чистого OOP.

Единственная проблема, которую я вижу в STL, и шаблоны в целом - ужасные сообщения об ошибках. Что будет решаться с помощью Concepts в С++ 0X.

Сравнение STL с коллекциями на Java - это как сравнение Тадж-Махала с моим домом:)

Ответ 6

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

Я думаю, вы неправильно понимаете предполагаемое использование понятий шаблонами. Форвардный итератор, например, является очень четкой концепцией. Чтобы найти выражения, которые должны быть действительными для того, чтобы класс был Forward Iterator и их семантика, включая вычислительную сложность, вы смотрите на стандарт или на http://www.sgi.com/tech/stl/ForwardIterator.html (вы должны следить за ссылками на Input, Output и Trivial Iterator, чтобы увидеть все).

Этот документ является совершенно хорошим интерфейсом, и здесь определяются "реальные детали концепции". Они не определяются реализациями Forward Iterators, и они не определяются алгоритмами, использующими Forward Iterators.

Различия в том, как интерфейсы обрабатываются между STL и Java, трижды:

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

2) Интерфейсы Java - это объекты времени исполнения, тогда как концепции STL не отображаются во время выполнения даже с RTTI.

3) Если вам не удалось выполнить допустимые допустимые выражения для концепции STL, вы получите неуказанную ошибку компиляции при создании экземпляра какого-либо шаблона с типом. Если вам не удалось реализовать требуемый метод интерфейса Java, вы получите конкретную ошибку компиляции, говоря так.

Эта третья часть - это если вам нравится (компиляция) "утиная печать": интерфейсы могут быть неявными. В Java интерфейсы несколько явны: класс "is" Iterable тогда и только тогда, когда он говорит, что он реализует Iterable. Компилятор может проверить, что подписи его методов все присутствуют и правильны, но семантика все еще неявная (т.е. они либо задокументированы, либо нет, но только больше кода (модульные тесты) могут рассказать вам, правильна ли реализация).

В С++, как и в Python, семантика и синтаксис являются неявными, хотя в С++ (и в Python, если вы получаете препроцессор с сильной типизацией) вы получаете некоторую помощь от компилятора. Если программисту требуется Java-подобное явное объявление интерфейсов с помощью класса-реализации, то стандартным подходом является использование признаков типа (и множественное наследование может помешать этому быть слишком многословным). Недостаток, по сравнению с Java, - это один шаблон, который я могу создать с помощью моего типа, и который будет компилироваться, если и только если все необходимые выражения действительны для моего типа. Это скажет мне, выполнил ли я все необходимые бит, "прежде чем использовать его". Это удобство, но это не ядро ​​ООП (и оно по-прежнему не проверяет семантику, а код для проверки семантики, естественно, также проверяет справедливость рассматриваемых выражений).

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

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

Лично я считаю, что неявные типы являются силой, когда они используются соответствующим образом. Алгоритм говорит, что он делает с его параметрами шаблона, и разработчик уверен, что все это работает: это именно общий знаменатель того, что нужно делать "интерфейсам". Кроме того, с STL вы вряд ли будете использовать, скажем, std::copy, основываясь на нахождении его прямого объявления в файле заголовка. Программисты должны разрабатывать, какую функцию выполняет на основе своей документации, а не только подписи функции. Это верно в С++, Python или Java. Существуют ограничения на то, что может быть достигнуто при наборе текста на любом языке, и попытка использовать типизацию для выполнения чего-то, чего она не делает (проверьте семантику), будет ошибкой.

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

Здесь Bjarne на явно объявленных интерфейсах: http://www.artima.com/cppsource/cpp0xP.html

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

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

Ответ 7

"ООП для меня означает только обмен сообщениями, локальное сохранение и защиту и скрытие процесса состояния, а также крайнее позднее связывание всех вещей. Это можно сделать в Smalltalk и в LISP. Возможно, существуют другие системы, в которых это возможно, но я не знаю о них". - Алан Кей, создатель Smalltalk.

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

Ответ 8

Основная проблема с

void MyFunc(ForwardIterator *I);

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

Ответ 9

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

Просто для предоставления другой справки:

Аль Стивенс Интервью Алекс Степанов, в марте 1995 года DDJ:

Степанов объяснил свой опыт работы и выбор в отношении большой библиотеки алгоритмов, которая в конечном итоге превратилась в STL.

Расскажите нам о своем долгосрочном интересе к универсальному программированию

..... Затем мне предложили работу в Bell Laboratories, работающей в группе С++ в библиотеках С++. Они спросили меня, могу ли я сделать это на С++. Конечно, я не знал С++ и, конечно, я сказал, что могу. Но я не мог сделать это на С++, потому что в 1987 году на С++ не было шаблонов, которые необходимы для включения этого стиля программирования. Наследование было единственным механизмом для получения общности, и этого было недостаточно.

Даже сейчас наследование С++ не очень полезно для общего программирования. Давайте обсудим, почему. Многие люди пытались использовать наследование для реализации структур данных и классов контейнеров. Как мы знаем сейчас, было мало успешных попыток. Наследование С++, а связанный с ним стиль программирования резко ограничен. Невозможно реализовать дизайн, который включает в себя тривиальную вещь, как использование равенства. Если вы начинаете с базового класса X в корне вашей иерархии и определяете оператор виртуального равенства в этом классе, который принимает аргумент типа X, то выведите класс Y из класса X. Каков интерфейс равенства? Он имеет равенство, которое сравнивает Y с X. Используя животных в качестве примера (люди ОО любят животных), определите млекопитающих и выведите жирафа у млекопитающих. Затем определите член-функцию-член, где животные согласуются с животными и возвращают животное. Затем вы получаете жирафа от животного и, конечно же, у него есть помощник по функциям, где жираф соединяется с животным и возвращает животное. Это определенно не то, что вы хотите. Хотя спаривание может быть не очень важно для программистов на C++, равенство есть. Я не знаю ни одного алгоритма, где не используется какое-либо равенство.

Ответ 10

Как вы делаете сравнения с ForwardIterator *? То есть, как вы проверяете, есть ли у вас предмет, который вы ищете, или вы его передали?

В большинстве случаев я бы использовал что-то вроде этого:

void MyFunc(ForwardIterator<MyType>& i)

что означает, что я знаю, что я указываю на MyType, и я знаю, как их сравнивать. Хотя это выглядит как шаблон, на самом деле это не так (ключевое слово "шаблон" ).

Ответ 11

У этого вопроса много замечательных ответов. Следует также отметить, что шаблоны поддерживают открытый дизайн. При текущем состоянии объектно-ориентированных языков программирования при работе с такими проблемами необходимо использовать шаблон посетителя, а истинный ООП должен поддерживать несколько динамических привязок. См. Открыть Multi-Методы для С++, P. Pirkelbauer, et.al. для очень интересного чтения.

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

template<class Value,class T>
Value euler_fwd(size_t N,double t_0,double t_end,Value y_0,const T& func)
    {
    auto dt=(t_end-t_0)/N;
    for(size_t k=0;k<N;++k)
        {y_0+=func(t_0 + k*dt,y_0)*dt;}
    return y_0;
    }

Обратите внимание, что эта функция также будет работать, если Value - это вектор какого-либо типа (не std::vector, который следует называть std::dynamic_array, чтобы избежать путаницы)

Если func мал, эта функция получит много преимуществ от вложения. Пример использования

auto result=euler_fwd(10000,0.0,1.0,1.0,[](double x,double y)
    {return y;});

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

Теперь у вас есть большое выражение в func, и вы используете решатель ODE во многих местах, поэтому ваш исполняемый файл повсеместно загрязняется с помощью экземпляров шаблонов. Что делать? Первое, что нужно заметить, это то, что работает обычный указатель функции. Затем вы хотите добавить currying, чтобы написать интерфейс и явное создание экземпляра

class OdeFunction
    {
    public:
        virtual double operator()(double t,double y) const=0;
    };

template
double euler_fwd(size_t N,double t_0,double t_end,double y_0,const OdeFunction& func);

Но описанное выше действие работает только для double, почему бы не написать интерфейс в качестве шаблона:

template<class Value=double>
class OdeFunction
    {
    public:
        virtual Value operator()(double t,const Value& y) const=0;
    };

и специализируется на некоторых общих типах значений:

template double euler_fwd(size_t N,double t_0,double t_end,double y_0,const OdeFunction<double>& func);

template vec4_t<double> euler_fwd(size_t N,double t_0,double t_end,vec4_t<double> y_0,const OdeFunction< vec4_t<double> >& func); // (Native AVX vector with four components)

template vec8_t<float> euler_fwd(size_t N,double t_0,double t_end,vec8_t<float> y_0,const OdeFunction< vec8_t<float> >& func); // (Native AVX vector with 8 components)

template Vector<double> euler_fwd(size_t N,double t_0,double t_end,Vector<double> y_0,const OdeFunction< Vector<double> >& func); // (A N-dimensional real vector, *not* `std::vector`, see above)

Если функция сначала была спроектирована вокруг интерфейса, тогда вам пришлось бы наследовать от этой ABC. Теперь у вас есть этот параметр, а также указатель на функцию, лямбда или любой другой объект функции. Ключевым моментом здесь является то, что мы должны иметь operator()(), и мы должны иметь возможность использовать некоторые арифметические операторы в своем возвращаемом типе. Таким образом, шаблонный механизм разрывается в этом случае, если С++ не имеет перегрузки оператора.

Ответ 12

Концепция ветки интерфейса от интерфейса и возможность замены реализаций не является неотъемлемой частью объектно-ориентированного программирования. Я верю, что это идея, которая была задумана в разработке на основе компонентов, например Microsoft COM. (См. мой ответ о том, что такое разработка, основанная на компонентах?) Растущая и изучая С++, люди были раздуты наследованием и полиморфизмом. Только в 90-х годах люди начали говорить "Программа к" интерфейсу ", а не к" реализации "и" предпочтению "композиции объекта над" наследованием класса "". (оба из которых цитируются из GoF кстати).

Затем Java появилась со встроенным сборщиком мусора и ключевым словом interface, и внезапно стало практически фактически разделяться интерфейс и реализация. Прежде чем вы это знаете, идея стала частью OO. С++, шаблоны и STL предшествуют всем этим.