Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does template substitution fail on a constructor unless I add brackets?

I am trying to understand why substitution fails on the following snippet unless brackets are added:

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;
}

See this example: https://godbolt.org/z/XPqHww

For example2 I am able to implicitly pass the C{} to the constructor of D without any error. For example I am not allowed to implicitly pass the A<int>{} until I add brackets.

What defines this behaviour?

like image 626
Yorek B Avatar asked Feb 20 '20 00:02

Yorek B


1 Answers

example is a function template and the function parameters depend on the template parameter. Therefore and since you did not specify any template arguments explicitly in the call example(A<int>{}, A<int>{});, when you call this function, template argument deduction is performed to figure out what type T should be for the call.

With a few exceptions template argument deduction requires that a T can be found such that the type of the argument in the function call matches the type in the function parameter exactly.

The issue with your call is that A<int> does clearly not match B<T> exactly for any T (and none of the exceptions apply either), so the call will fail.

This behavior is necessary, because the compiler would otherwise need to test all possible types T to check whether the function can be called. That would be computationally infeasible or impossible.


In the call example2(C{}, C{}); no templates are involved, so no template argument deduction is performed. Because the compiler doesn't need to figure out the target type in the parameter anymore, it becomes feasible to consider implicit conversions from the known argument type to the known parameter type. One such implicit conversion is the construction of D from C via the non-explicit constructor D(C);. So the call succeeds with that conversion.

example2(C{}, {C{}}); does effectively the same.


The question then is why example(A<int>{}, {A<int>{}}); works. This is because of a specific rule that can be found e.g. in [temp.deduct.type]/5.6 of the C++17 standard (draft N4659). It says that a function argument/parameter pair for which the argument is an initializer list (i.e. {A<int>{}}) and the parameter is not a specialization of std::initializer_list or an array type (it is neither here), the function parameter is a non-deduced context.

Non-deduced context means that the function argument/parameter pair will not be used during template argument deduction to figure out the type of T. This means its type does not need to match exactly. Instead, if template argument deduction otherwise succeeds, the resulting T will simply be substituted into the non-deduced context and from there implicit conversions will be considered as before. B<T> can be constructed from {A<int>} if T = int because of the non-explicit constructor B(A<T>);.

Now the question is whether template argument deduction will succeed and deduce T = int. It can only succeed if a T can be deduced from another function parameter. And indeed there is still the first parameter, for which the types match exactly: A<int>/A<T> matches for T = int and because this function argument doesn't use an initializer list, it is a context from which T will be deduced.

So indeed for example(A<int>{}, {A<int>{}}); deduction from the first argument will yield T = int and substitution into the second parameter B<T> makes the initialization/conversion B<T>{A<int>{}} succeed, so that the call is viable.


If you were to use initializer lists for both parameters as in example({A<int>{}}, {A<int>{}});, both argument/parameter pairs become non-deduced context and there wouldn't be anything left to deduce T from and so the call would fail for failure to deduce T.


You can make all calls work by specifying T explicitly, so that template argument deduction becomes unnecessary, e.g.:

example<int>(A<int>{}, A<int>{});
like image 164
walnut Avatar answered Sep 27 '22 15:09

walnut