Можно ли при выводе параметра шаблонной функции рассчитывать на неявное преобразование производного класса к базовому?
Нет. При выводе аргументов шаблона компилятор сопоставляет тип аргумента с шаблонным параметром и обычно не применяет преобразование производного класса к базовому. Такое преобразование может быть выполнено позже, на этапе обычной проверки совместимости уже выведенной функции; явное указание параметра шаблона также может сделать вызов допустимым.
Вывод аргументов шаблона создавался как механизм структурного сопоставления типов, а не как общий поиск последовательности преобразований. Это делает выбор специализации и перегрузки предсказуемым: компилятор сначала устанавливает параметры шаблона, а затем проверяет, можно ли передать аргумент получившемуся параметру функции.
Если бы при выводе свободно учитывались преобразования, один аргумент мог бы подходить к множеству шаблонных параметров через разные цепочки преобразований. Это усложнило бы правила выбора и сделало результат зависимым от деталей перегрузки.
Рассмотрим шаблон, принимающий специализацию базового класса, и тип, производный от этой специализации. Логически объект производного типа можно передать функции, принимающей ссылку на базовый класс, но для вывода параметра шаблона компилятору сначала нужно определить аргумент шаблона.
Если вывод не удался, до проверки преобразования дело не доходит. В результате вызов может завершиться ошибкой, хотя после подстановки конкретного аргумента шаблона обычное преобразование производного класса к базовому было бы корректным.
При выводе компилятор сравнивает образ параметра функции с типом аргумента. Для параметра вида Base<T> и аргумента типа Derived, где Derived наследуется от Base<int>, он не извлекает T = int через отношение наследования. Типы не совпадают по требуемой структуре, поэтому вывод завершается неудачей.
В первом вызове параметр T неизвестен, а преобразование Derived к Base<int> не участвует в выводе. Во втором вызове T задан явно, параметр функции становится const Base<int>&, и после этого обычное преобразование производного класса к базовому разрешает передачу аргумента.
Важно отличать вывод от последующей проверкой вызова. После успешного вывода компилятор может применять разрешённые неявные преобразования, включая преобразование ссылки или указателя на производный класс к соответствующему базовому. Но преобразования не помогают определить сам неизвестный параметр шаблона.
Есть стандартные корректировки, являющиеся частью правил вывода: например, затухание массива или функции для параметра, передаваемого по значению, а также обработка некоторых cv-квалификаторов. Они не превращают вывод в общий механизм неявных преобразований и не отменяют ограничение для производного и базового классов.
Практические варианты решения имеют разные свойства:
В библиотеке сериализации был шаблон, принимающий Serializable<T>, а пользователь передавал объект класса-адаптера, производного от Serializable<Record>. Разработчик ожидал, что компилятор найдёт T = Record через базовый класс, но шаблонная функция не компилировалась: вывод не учитывал преобразование к базовому типу.
Явная передача Record решила проблему, но сделала вызовы громоздкими. Перегрузка для базового интерфейса сохранила удобство, однако потребовала проверить возможную неоднозначность с другими перегрузками. В итоге библиотека приняла базовый интерфейс без выводимого параметра в публичной функции, а тип Record передавался во внутренний шаблон уже после успешного вызова; это отделило полиморфное преобразование от задачи вывода и сделало API устойчивее.
Всегда ли явное указание параметра шаблона разрешает преобразование к базовому типу?
Нет, оно только устраняет необходимость вывести этот параметр. После подстановки должен существовать допустимый путь преобразования, а доступность базового класса, cv-квалификаторы, тип ссылки и остальные правила передачи аргумента всё равно проверяются. Если базовый класс недоступен или преобразование неоднозначно, явное указание не исправит вызов.
Чем отличается параметр функции, содержащий сам T, от параметра вида Base<T>?
В случае T и объекта производного типа обычно выводится фактический тип объекта, то есть производный тип. В случае Base<T> компилятор должен сопоставить структуру аргумента с шаблонной специализацией Base, а производный тип не считается такой специализацией для целей вывода; наследование не используется для извлечения T.
Может ли пользовательское преобразование помочь выводу параметра шаблона?
Нет. Пользовательские преобразования не участвуют в выводе аргументов шаблона. Даже если объект имеет оператор преобразования к нужной специализации базового типа, шаблонная функция не получит из этого преобразования неизвестный аргумент. Нужно либо указать аргумент шаблона явно, либо предварительно выполнить преобразование, либо изменить интерфейс функции так, чтобы выводимый параметр не зависел от результата преобразования.