example
- это шаблон функции, а параметры функции зависят от параметра шаблона. Следовательно, и поскольку вы не указали явно никаких аргументов шаблона в вызове example(A<int>{}, A<int>{});
, при вызове этой функции вычет аргумента шаблона выполняется для выяснения, какой тип T
должен быть для вызова.
За некоторыми исключениями для вывода аргумента шаблона требуется найти T
, чтобы тип аргумента в вызове функции совпадал с типом в параметре функции точно .
Проблема с вашим вызовом в том, что A<int>
явно не соответствует B<T>
точно для любого T
(и ни одно из исключений также не применимо), поэтому вызов не будет выполнен.
Это поведение необходимо, потому что в противном случае компилятор должен протестировать все возможные типы T
, чтобы проверить, может ли функция быть вызвана. Это было бы невозможно с вычислительной точки зрения или невозможно.
В вызове example2(C{}, C{});
нет шаблонов, поэтому вывод аргументов шаблона не производится. Поскольку компилятору больше не нужно определять целевой тип в параметре, становится возможным рассмотреть неявные преобразования из известного типа аргумента в известный тип параметра. Одним из таких неявных преобразований является построение D
из C
через неявный конструктор D(C);
. Таким образом, вызов преуспевает с этим преобразованием.
example2(C{}, {C{}});
делает то же самое.
Тогда возникает вопрос, почему example(A<int>{}, {A<int>{}});
работает. Это связано с определенным правилом c, которое можно найти, например, в [temp.deduct.type] /5.6 стандарта C ++ 17 (черновик N4659). В нем говорится, что пара аргумент / параметр функции, для которой аргумент является списком инициализатора (т.е. {A<int>{}}
), а параметр не является специализацией std::initializer_list
или типом массива (его здесь тоже нет) параметр функции представляет собой невыгруженный контекст .
невыгруженный контекст означает, что пара аргумент / параметр функции будет не используется во время вывода аргумента шаблона для определения типа T
. Это означает, что его тип не обязательно должен совпадать. Вместо этого, если в противном случае вывод аргумента шаблона будет успешным, результирующее T
будет просто подставлено в невыгруженный контекст, и оттуда неявные преобразования будут рассматриваться как прежде. B<T>
может быть построен из {A<int>}
если T = int
из-за неявного конструктора B(A<T>);
.
Теперь вопрос состоит в том, удастся ли вывести аргумент шаблона и вывести T = int
. Это может быть успешно выполнено, только если T
может быть выведено из другого параметра функции. И действительно, есть еще первый параметр, для которого типы точно совпадают: A<int>
/ A<T>
соответствует T = int
, и поскольку этот аргумент функции не использует список инициализатора, это контекст, из которого T
Таким образом, для example(A<int>{}, {A<int>{}});
вычет из первого аргумента даст T = int
, а подстановка во второй параметр B<T>
сделает успешной инициализацию / преобразование B<T>{A<int>{}}
, так что вызов viable.
Если бы вы использовали списки инициализаторов для обоих параметров, как в example({A<int>{}}, {A<int>{}});
, обе пары аргумент / параметр становятся не выводимым контекстом и ничего не будет осталось вывести T
из и, таким образом, вызов не будет выполнен из-за невозможности вывести T
.
Вы можете заставить все вызовы работать, указав T
в явном виде, так что вывод аргумента шаблона становится ненужным Например:
example<int>(A<int>{}, A<int>{});