Большинство реализаций дженериков (или, скорее, параметрического полиморфизма) действительно используют стирание типов. Это значительно упрощает проблему компиляции универсального кода, но работает только для упакованных типов: поскольку каждый аргумент фактически является непрозрачным указателем, нам необходим VTable или подобный механизм диспетчеризации для выполнения операций над аргументами. В Java:
<T extends Addable> T add(T a, T b) { … }
можно скомпилировать, проверить тип и вызвать так же, как
Addable add(Addable a, Addable b) { … }
за исключением того, что дженерики предоставляют контролеру типов гораздо больше информации на сайте вызова. Эта дополнительная информация может быть обработана с помощью переменных типа , особенно когда выводятся универсальные типы. Во время проверки типа каждый универсальный тип может быть заменен переменной, давайте назовем это $T1
:
$T1 add($T1 a, $T1 b)
Затем переменная типа обновляется с появлением большего числа фактов, пока они не могут быть заменены конкретным типом. Алгоритм проверки типа должен быть написан так, чтобы он соответствовал этим переменным типа, даже если они еще не разрешены до полного типа. В самой Java это обычно можно легко сделать, так как тип аргументов часто известен до того, как должен быть известен тип вызова функции. Заметным исключением является лямбда-выражение в качестве аргумента функции, которое требует использования переменных типа.
Намного позже, оптимизатор может генерировать специализированный код для определенного набора аргументов, тогда это будет фактически своего рода встраиванием.
VTable для аргументов общего типа можно избежать, если универсальная функция не выполняет никаких операций над типом, а только передает их другой функции. Например, функция Haskell call :: (a -> b) -> a -> b; call f x = f x
не должна была бы блокировать x
аргумент. Однако для этого требуется соглашение о вызовах, которое может проходить через значения, не зная их размера, что в любом случае существенно ограничивает его указателями.
С ++ сильно отличается от большинства языков в этом отношении. Шаблонный класс или функция (я буду обсуждать только шаблонные функции здесь) сам по себе не вызывается. Вместо этого шаблоны следует понимать как мета-функцию времени компиляции, которая возвращает фактическую функцию. На мгновение игнорируя вывод аргументов шаблона, общий подход сводится к следующим шагам:
Примените шаблон к предоставленным аргументам шаблона. Например, вызов template<class T> T add(T a, T b) { … }
as add<int>(1, 2)
даст нам реальную функцию int __add__T_int(int a, int b)
(или какой-либо другой подход к именованию).
Если код для этой функции уже был сгенерирован в текущем модуле компиляции, продолжайте. В противном случае, сгенерируйте код, как если бы функция int __add__T_int(int a, int b) { … }
была написана в исходном коде. Это включает в себя замену всех вхождений аргумента шаблона его значениями. Это, вероятно, преобразование AST → AST. Затем выполните проверку типа сгенерированного AST.
Скомпилируйте вызов, как если бы исходный код был __add__T_int(1, 2)
.
Обратите внимание, что шаблоны C ++ имеют сложное взаимодействие с механизмом разрешения перегрузки, который я не хочу описывать здесь. Также обратите внимание, что эта генерация кода делает невозможным использование шаблонного метода, который также является виртуальным - подход, основанный на стирании типов, не страдает от этого существенного ограничения.
Что это значит для вашего компилятора и / или языка? Вы должны тщательно продумать, какие дженерики вы хотите предложить. Стирание типов при отсутствии вывода типов - самый простой из возможных подходов, если вы поддерживаете коробочные типы. Специализация шаблонов кажется довольно простой, но обычно включает искажение имен и (для нескольких блоков компиляции) существенное дублирование в выходных данных, поскольку шаблоны создаются на сайте вызовов, а не на сайте определения.
Подход, который вы показали, по сути является C ++ - подобным шаблонным подходом. Тем не менее, вы сохраняете специализированные / созданные экземпляры шаблонов как «версии» основного шаблона. Это вводит в заблуждение: концептуально они не одинаковы, и разные экземпляры функции могут иметь совершенно разные типы. Это усложнит ситуацию в долгосрочной перспективе, если вы также допустите перегрузку функций. Вместо этого вам потребуется понятие набора перегрузки, содержащего все возможные функции и шаблоны, которые имеют общее имя. За исключением разрешения перегрузки, вы можете рассматривать различные созданные шаблоны как полностью отдельные друг от друга.