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?
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>{});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With