LINUX.ORG.RU

История изменений

Исправление Miguel, (текущая версия) :

Т.е. дженерики - это штуки из явы, которые Array<A> array ?

Ага.

Всегда для себя считал что шаблоны с++ это как раз то что называется Generics в ява (и в общем-то использовал их именно так, правда практики немного).

А вот это — неправда. Хотя соглашусь, области их использования во многом пересекаются.

Фишка вот в чём. Дженерик (в жабе, скале, окамле, хаскеле и прочих нормальных языках) — это одна функция, которая готова работать с разными типами. Она может быть скомпилирована отдельно, и после этого принимать ЛЮБОЙ тип, который соответствует заданным — при её создании — требованиям. То есть, компилятор, конечно, вправе сделать особые, оптимизированные её версии для разных типов (и, скажем, GHC так и делает), но для программиста это всё прозрачно: одна функция. А самое важное — семантика этой функции определена в терминах производимых ею действий.

Шаблон же — это такой макрос. Если ты используешь его с новым типом, то где-то за занавесом создаётся новая функция, специально для работы с этим типом. И семантика шаблона определяется в терминах того КОДА, который он генерирует. Код при этом может лишь внешне выглядеть похоже, а работать совсем по-другому.

Отсюда сильные и слабые стороны тех и других.

1) Шаблон невозможно скомпилировать отдельно. Весь код шаблона должен быть доступен всякий раз, когда он используется. Иначе невозможно будет по этому образцу построить код настоящей функции. Именно поэтому вся реализация шаблонов пихается в .h-файлы. Теоретически, есть такая вещь как «раздельная компиляция шаблонов» (в некоторых компиляторах поддерживается); практически её нет, и весь исходный код шаблона всё равно должен быть доступен.

2) Шаблоны не поддерживают полиморфную рекурсию. Допустим, для того, чтобы вычислить f<Type>(x), тебе нужно ПОРОЙ (не всегда) вычислять f<X<Type>>(...). С дженериками проблемы нет, если только ты можешь гарантировать, что X<Type> удовлетворяет всем необходимым ограничениям — функция-то одна. А вот шаблон, попытавшись сгенерировать код для f<Type>, наткнётся на необходимость сгенерировать код для f<X<Type>> (неважно, что он не всегда используется, код-то генерируется не в рантайме), а для этого нужно сгенерировать код для f<X<X<Type>>>... и мы упираемся в переполнение стека на этапе компиляции.

3) Дженерики не поддерживают частичную специализацию. Функция должна быть одна, и ты не можешь вдруг сказать «а, для типа int мы напишем другую реализацию, получше». То есть, можешь — но тебе нужно предусмотреть это изначально, при создании дженерика. Когда дженерик скомпилирован — всё, менять в нём уже ничего нельзя.

4) Сообщения об ошибках. Дженерик проверяет корректность кода сразу. Шаблон проверяет только синтаксис. О том, что, оказывается, требуется, чтобы тип-аргумент шаблона поддерживал какой-то специфический метод, ты узнаешь только тогда, когда попытаешься использовать шаблон — или когда прочитаешь весь его код. С дженериком достаточно знать его заголовок, там — все необходимые требования. И о том, что дженерик написан некорректно, ты узнаешь сразу, как только попытаешься скомпилировать файл, в котором он определён.

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

Т.е. создатель типа должен позаботиться обо всех интерфейсах, которые будет реализовывать тип.

Ну, это жабопроблемы. В хаскеле, например, вполне можно реализовать новый интерфейс уже после того, как создан тип. А Скала более-менее имитирует такую возможность, позволяя создавать собственные неявные преобразования, из одного типа в другой.

Исходная версия Miguel, :

Т.е. дженерики - это штуки из явы, которые Array<A> array ?

Ага.

Всегда для себя считал что шаблоны с++ это как раз то что называется Generics в ява (и в общем-то использовал их именно так, правда практики немного).

А вот это — неправда. Хотя соглашусь, области их использования во многом пересекаются.

Фишка вот в чём. Дженерик (в жабе, скале, окамле, хаскеле и прочих нормальных языках) — это одна функция, которая готова работать с разными типами. Она может быть скомпилирована отдельно, и после этого принимать ЛЮБОЙ тип, который соответствует заданным — при её создании — требованиям. То есть, компилятор, конечно, вправе сделать особые, оптимизированные её версии для разных типов (и, скажем, GHC так и делает), но для программиста это всё прозрачно: одна функция. А самое важное — семантика этой функции определена в терминах производимых ею действий.

Шаблон же — это такой макрос. Если ты используешь его с новым типом, то где-то за занавесом создаётся новая функция, специально для работы с этим типом. И семантика шаблона определяется в терминах того КОДА, который он генерирует. Код при этом может лишь внешне выглядеть похоже, а работать совсем по-другому.

Отсюда сильные и слабые стороны тех и других.

1) Шаблон невозможно скомпилировать отдельно. Весь код шаблона должен быть доступен всякий раз, когда он используется. Иначе невозможно будет по этому образцу построить код настоящей функции. Именно поэтому вся реализация шаблонов пихается в .h-файлы. Теоретически, есть такая вещь как «раздельная компиляция шаблонов» (в некоторых компиляторах поддерживается); практически её нет, и весь исходный код шаблона всё равно должен быть доступен.

2) Шаблоны не поддерживают полиморфную рекурсию. Допустим, для того, чтобы вычислить f<Type>(x), тебе нужно ПОРОЙ (не всегда) вычислять f<X<Type>>(...). С дженериками проблемы нет, если только ты можешь гарантировать, что X<Type> удовлетворяет всем необходимым ограничениям — функция-то одна. А вот шаблон, попытавшись сгенерировать код для f<Type>, наткнётся на необходимость сгенерировать код для f<X<Type>> (неважно, что он не всегда используется, код-то генерируется не в рантайме), а для этого нужно сгенерировать код для f<X<X<Type>>>... и мы упираемся в переполнение стека на этапе компиляции.

3) Дженерики не поддерживают частичную специализацию. Функция должна быть одна, и ты не можешь вдруг сказать «а, для типа int мы напишем другую реализацию, получше». То есть, можешь — но тебе нужно предусмотреть это изначально, при создании дженерика. Когда дженерик скомпилирован — всё, менять в нём уже ничего нельзя.

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

Т.е. создатель типа должен позаботиться обо всех интерфейсах, которые будет реализовывать тип.

Ну, это жабопроблемы. В хаскеле, например, вполне можно реализовать новый интерфейс уже после того, как создан тип. А Скала более-менее имитирует такую возможность, позволяя создавать собственные неявные преобразования, из одного типа в другой.