Почему подстановка шаблона завершается неудачно в конструкторе, если я не добавляю скобки? - PullRequest
6 голосов
/ 20 февраля 2020

Я пытаюсь понять, почему замена не выполняется в следующем фрагменте, если не добавлены скобки:

template<typename T>
struct A {};

template<typename T>
struct B {
    B(A<T>);
};

template<typename T>
void example(A<T>, B<T>);

struct C {};

struct D {
    D(C);
};

void example2(C, D);

int main(int argc, char *argv[]) {
    example(A<int>{}, A<int>{}); // error
    example(A<int>{}, {A<int>{}}); // ok

    example2(C{}, C{}); // ok
    example2(C{}, {C{}}); // ok

    return 0;
}

См. Этот пример: https://godbolt.org/z/XPqHww

Для example2 Я могу безоговорочно передать C{} в конструктор D без каких-либо ошибок. Для example мне не разрешено неявно передавать A<int>{}, пока я не добавлю скобки.

Что определяет это поведение?

1 Ответ

7 голосов
/ 20 февраля 2020

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>{});
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...