Почему C ++ STL так сильно основан на шаблонах? (а не на * интерфейсах *) - PullRequest
207 голосов
/ 24 июня 2009

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

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)

Ответы [ 13 ]

588 голосов
/ 24 июня 2009

Краткий ответ: «потому что 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 не работал с собственными массивами, он был бы бесполезным .

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

86 голосов
/ 24 июня 2009

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

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

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

71 голосов
/ 24 июня 2009

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

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

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

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

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

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

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

23 голосов
/ 24 июня 2009

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

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

18 голосов
/ 24 июня 2009

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

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

на ваш вопрос,

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

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

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

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

11 голосов
/ 24 июня 2009

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

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

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

Различия в том, как обрабатываются интерфейсы между 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 только глядя на его определение, где вам нужно либо посмотреть на реализация или документация для ...

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

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

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

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

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

8 голосов
/ 24 июня 2009

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

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

6 голосов
/ 28 июля 2009

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

Просто чтобы предоставить другую ссылку:

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

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

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

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

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

5 голосов
/ 24 июня 2009

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

void MyFunc(ForwardIterator *I);

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

2 голосов
/ 17 марта 2018

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

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

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

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

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...