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


211

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

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

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

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

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

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

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

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

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

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

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

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


4
Вы можете проверить stackoverflow.com/questions/31693/… . Принятый ответ является отличным объяснением того, что шаблоны предлагают вам по сравнению с дженериками.
Джеймс МакМэхон

6
@Jonas: это не имеет смысла. Ограничение на кеш стоит тактов, поэтому это важно. В конце концов, именно такты, а не кэш, определяют производительность. Память и кеш важны только в той мере, в которой они влияют на затраченные тактовые циклы. Более того, эксперимент можно сделать легко. Сравните, скажем, std :: for_Each, вызываемый с аргументом функтора, с эквивалентным подходом OOP / vtable. Разница в производительности ошеломляет . Именно поэтому версия шаблона используется.
Джалф

7
и нет никаких причин, почему избыточный код будет заполнять icache. Если я создаю экземпляр вектора <char> и вектора <int> в моей программе, почему код вектора <char> должен загружаться в icache во время обработки вектора <int>? Фактически, код для вектора <int> обрезается, потому что он не должен включать в себя код для приведения, vtables и косвенного обращения.
Джалф

3
Алексей Степанов объясняет, почему наследование и равенство плохо сочетаются друг с другом.
fredoverflow

6
@BerndJendrissek: Хм, близко, но не вы сами. Да, больше кода стоит с точки зрения пропускной способности памяти и использования кэша, если он вообще используется . Но нет особой причины ожидать vector<int>и vector<char>использоваться одновременно. Они могут, конечно, но вы можете использовать любые две части кода одновременно. Это не имеет ничего общего с шаблонами, C ++ или STL. В создании экземпляра нет ничего, vector<int>что требовало vector<char>бы загрузки или выполнения кода.
Джалф

Ответы:


607

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


22
Это хорошая рецензия, но я бы хотел выделить одну деталь. STL не является «продуктом» C ++. Фактически, STL, как концепция, существовал до C ++, а C ++ оказался эффективным языком, обладающим (почти) достаточной мощностью для общего программирования, поэтому STL был написан на C ++.
Игорь Кривокон

17
Поскольку комментарии продолжают поднимать это, да, я знаю, что имя STL неоднозначно. Но я не могу придумать лучшего названия для «части стандартной библиотеки C ++, которая смоделирована на STL». Фактическое название для этой части стандартной библиотеки - просто «STL», хотя оно и является строго неточным. :) Пока люди не используют STL в качестве имени для всей стандартной библиотеки (включая IOStreams и заголовки C stdlib), я счастлив. :)
Джалф

5
@einpoklum А что бы вы получили от абстрактного базового класса? Взять хотя std::setбы пример. Он не наследуется от абстрактного базового класса. Как это ограничивает ваше использование std::set? Есть ли что-то, что вы не можете сделать с a, std::setпотому что оно не наследуется от абстрактного базового класса?
fredoverflow

22
@einpoklum, пожалуйста, взгляните на язык Smalltalk, который Алан Кей разработал как язык ООП, когда изобрел термин ООП. У него не было интерфейсов. ООП не об интерфейсах или абстрактных базовых классах. Собираетесь ли вы сказать, что «Java, которая не имеет ничего общего с тем, что имел в виду изобретатель термина ООП, является больше ООП, чем С ++, что также не похоже на то, что имел в виду изобретатель термина ООП»? Что вы хотите сказать, это "C ++ не достаточно Java-как на мой вкус". Это справедливо, но это не имеет ничего общего с ООП.
Джалф

8
@MasonWheeler, если бы этот ответ был откровенной чепухой, вы бы не увидели, как буквально сотни разработчиков по всему миру проголосовали бы +1 за это, и только три человека поступили бы иначе
panda-34

88

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

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

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


4
«а с C ++ 0x некоторые вещи могут быть даже запрограммированы функционально» - это может быть запрограммировано функционально без этих функций, просто более подробно.
Йонас Кёлкер

3
@ Tyler Действительно, если вы ограничите C ++ до чистого ООП, вы останетесь с Objective-C.
Юстиция

@TylerMcHenry: Просто спросив это , я обнаружил, что только что сказал тот же ответ, что и вы! Всего одна точка. Я хотел бы добавить, что стандартную библиотеку нельзя использовать для написания объектно-ориентированного кода.
einpoklum

74

Насколько я понимаю, Страуструп изначально предпочитал конструкцию контейнера в стиле «ООП» и фактически не видел другого способа сделать это. Александр Степанов является ответственным за STL, и в его задачи не входило «сделать его объектно-ориентированным» :

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

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

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

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

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

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


2
Интересное интервью. Я уверен, что прочитал это раньше, но определенно стоило повторить. :)
jalf

3
Одно из самых интересных интервью о программировании, которое я когда-либо читал. Хотя это оставляет меня жаждать более подробной информации ...
Феликсиз

Многие жалобы, которые он предъявляет в отношении таких языков, как Java («Вы не можете написать универсальный max () в Java, который принимает два аргумента некоторого типа и имеет возвращаемое значение того же типа»), относились только к очень ранним версиям языка, прежде чем были добавлены дженерики. Даже с самого начала было известно, что дженерики в конечном итоге будут добавлены, хотя (как только будет определен жизнеспособный синтаксис / семантика), поэтому его критика в основном беспочвенна. Да, дженерики в той или иной форме необходимы для сохранения безопасности типов в статически типизированном языке, но нет, это не делает ОО бесполезным.
Какой-то парень

1
@SomeGuy Они не жалуются на Java как таковые. Он говорит о « стандартном» ОО-программировании SmallTalk или, скажем, Java ». Интервью с конца 90-х (он упоминает о работе в SGI, которую он оставил в 2000 году для работы в AT & T). Обобщения были добавлены в Java только в 2004 году в версии 1.5, и они являются отклонением от «стандартной» модели ОО.
Мельпомена

24

Ответ можно найти в этом интервью со Степановым, автором STL:

Да. STL не является объектно-ориентированным. Я думаю, что объектная ориентация - почти такая же обман, как и искусственный интеллект. Мне еще предстоит увидеть интересный кусок кода, который исходит от этих OO людей.


Хороший драгоценный камень; Вы знаете, с какого года?
Кос

2
@Kos, согласно web.archive.org/web/20000607205939/http://www.stlport.org/… первая версия связанной страницы от 7 июня 2001 года. На самой странице внизу написано Copyright 2001- 2008.
alfC

@Кос Степанов в первом ответе упоминает работу в SGI. Он покинул SGI в мае 2000 года, так что, по-видимому, интервью старше этого.
Мельпомена

18

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

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

на твой вопрос,

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

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

Единственная проблема, которую я вижу с STL и шаблонами в целом, это ужасные сообщения об ошибках. Который будет решен с использованием концепций в C ++ 0X.

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


12
Что, Тадж Махал маленький и элегантный, а твой дом размером с гору и в полном беспорядке? ;)
jalf

Концепции больше не являются частью c ++ 0x. Некоторые сообщения об ошибках могут быть прерваны с помощью static_assertвозможно.
KitsuneYMG

GCC 4.6 улучшил шаблоны сообщений об ошибках, и я считаю, что 4.7+ еще лучше с ним.
Дэвид Стоун

Концепция - это, по сути, «интерфейс», о котором спрашивал ОП. Единственное отличие состоит в том, что «наследование» от Концепции является неявным (если класс имеет все нужные функции-члены, это автоматически является подтипом Концепции), а не явным (класс Java должен явно объявить, что он реализует интерфейс) , Тем не менее, как неявные, так и явные подтипы являются допустимыми OO, и некоторые языки OO имеют неявное наследование, которое работает так же, как Concepts. Итак, здесь говорится в основном: «ОО отстой: используйте шаблоны. Но у шаблонов есть проблемы, поэтому используйте концепции (которые являются ОО)».
Какой-то парень

11

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


На явно объявленных интерфейсах два слова: классы типов. (Это уже то, что Степанов подразумевает под «концепцией».)
пион,

«Если вам не удастся сделать правильными требуемые допустимые выражения для концепции STL, вы получите неопределенную ошибку компиляции, когда создадите какой-то шаблон с типом». - это ложь. Передача чего-либо в stdбиблиотеку, которая не соответствует концепции, обычно "плохо сформирована, диагностика не требуется".
Якк - Адам Невраумонт

Правда, я играл быстро и свободно с термином «действительный». Я просто имел в виду, что если компилятор не может скомпилировать одно из обязательных выражений, он сообщит о чем-то.
Стив Джессоп

8

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

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


7

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

Просто чтобы дать еще одну ссылку:

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

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

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

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

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


5

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

void MyFunc(ForwardIterator *I);

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


1
Ну, я тоже: 1. Не пытайтесь получить это, так как я пишу общий код. Или 2. Получите его, используя любой механизм отражения, который C ++ предлагает сегодня.
einpoklum

2

Давайте подумаем о стандартной библиотеке как о базе данных коллекций и алгоритмов.

Если вы изучали историю баз данных, вы, несомненно, знаете, что в самом начале базы данных были в основном «иерархическими». Иерархические базы данных очень близко соответствовали классическим ООП - в частности, разновидности с единым наследованием, например, используемой Smalltalk.

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

Итак, они изобрели базы данных сетевой модели. Базы данных сетевой модели очень близко соответствуют множественному наследованию. C ++ полностью поддерживает множественное наследование, в то время как Java поддерживает ограниченную форму (вы можете наследовать только от одного класса, но также можете реализовать столько интерфейсов, сколько захотите).

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

Большая часть реляционных баз данных стала универсальной. Реляционная модель функционально является надмножеством сетевой модели (которая, в свою очередь, является надмножеством иерархической модели).

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

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

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


0

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

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

void MyFunc(ForwardIterator<MyType>& i)

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


вы можете просто использовать операторы типа <,> и = и не знать, что это такое (хотя это может быть не то, что вы имели в виду)
lhahne

В зависимости от контекста, они могут не иметь никакого смысла, или они могут работать нормально. Трудно сказать, не зная больше о MyType, который, по-видимому, делает пользователь, а мы нет.
Tanktalus

0

На этот вопрос есть много хороших ответов. Также следует отметить, что шаблоны поддерживают открытый дизайн. При текущем состоянии объектно-ориентированных языков программирования при работе с такими проблемами необходимо использовать шаблон посетителя, и истинный ООП должен поддерживать множественное динамическое связывание. См. Open Multi-Methods for C ++, 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-решатель во многих местах, поэтому ваш исполняемый файл повсеместно загрязнен экземплярами шаблонов. Что делать? Первое, на что нужно обратить внимание, это то, что обычный указатель функции работает. Затем вы хотите добавить карри, чтобы вы написали интерфейс и явную реализацию

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


-1

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

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


Договорились, что интерфейсы не просто ОО. Но способность полиморфизма в системе типов есть (это было в Симуле в 60-х годах). Интерфейсы модулей существовали в Modula-2 и Ada, но я думаю, что они работали в системе типов иначе.
andygavin
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.