Reputation: 5039
Consider the following code:
#include <utility>
#include <initializer_list>
template <typename T>
struct S {
S(std::initializer_list<std::pair<T, int>>) {};
};
int main()
{
S s1 {{42, 42}}; // failed due to implicit `std::pair` from `{42, 42}`
S s2 {std::pair{42, 42}}; // ok
}
s1
instance failed to compile due to implicit std::pair
created from the brace-initializer list.
Is there a way (by user-defined type deduction guide maybe) to make s1
compiliable without specify additional types in declaration?
Upvotes: 7
Views: 780
Reputation: 28396
The reason why your attempt cannot work, is exactly the same reason why this works:
#include <array>
#include <iostream>
#include <utility>
#include <initializer_list>
struct S1 {
S1(std::initializer_list<std::array<int,2>>) {
std::cout << "S1\n";
};
};
struct S2 {
S2(std::initializer_list<std::pair<int, int>>) {
std::cout << "S2\n";
};
};
int main()
{
S1 s1 {{42, 42}}; // Prints S1
S2 s2 {{42, 42}}; // Prints S2
}
Upvotes: 0
Reputation: 19113
Sadly no.
The reason is that template arguments cannot be deduced through nested braces (std::initializer_list
) because they occur in non-deduced context. See the link for more examples of this behaviour.
To not drag CTAD into this, your example is equivalent to:
#include <utility>
#include <initializer_list>
template <typename T>
void S(std::initializer_list<std::pair<T, int>>) {};
int main()
{
S({{42, 42}});
S ({std::pair{42, 42}});
}
Now let's see what why exactly these examples fail. The Standard says about the arguments deduction the following: (emphasis mine)
Template argument deduction is done by comparing each function template parameter type (call it P) that contains template-parameters that participate in template argument deduction with the type of the corresponding argument of the call (call it A) as described below. If removing references and cv-qualifiers from P gives std::initializer_list<P'> or P'[N] for some P' and N and the argument is a non-empty initializer list ([dcl.init.list]), then deduction is performed instead for each element of the initializer list, taking P' as a function template parameter type and the initializer element as its argument, and in the P'[N] case, if N is a non-type template parameter, N is deduced from the length of the initializer list. Otherwise, an initializer list argument causes the parameter to be considered a non-deduced context[temp.deduct.call]
So, because S
accepts the initializer list, the compiler first tries to deduce its inner type from each argument in the list and they must match. This means this helper function is created for the purposes of deduction:
template <typename X> void foo(X element);
and called with the inner list elements, {42,42}
in our case, leading to foo({42,42})
. Here lies the problem, you cannot deduce X
from this, the crucial thing is there is not even any information about std::pair
, so this task is simply impossible and explicitly disallowed by the Standard as a non-deduced context:
The non-deduced contexts are:
...
5.6 A function parameter for which the associated argument is an initializer list ([dcl.init.list]) but the parameter does not have a type for which deduction from an initializer list is specified ([temp.deduct.call]). [ Example:
template void g(T); g({1,2,3}); // error: no argument deduced for T
— end example ] [temp.deduct.type]
Now it should be clear why S({std::pair{42, 42}})
will work. Because the arguments of the outer list are given as std::pair<int,int>
(deduced by CATD before the call), the type passed to foo is then simply X=std::pair<int,int>
. The list std::initializer_list<std::pair<int,int>>
can now be matched against the function declaration to deduce T=int
, thus leading to a successful call.
All inner elements try to deduce X
independently if at least one succeeds, those who did not must be at least implicitly convertible. If more of them succeeded, they must have deduced the exact same type.
S({{42, 42}, std::pair{1,2},{2,3}}); // Works
// Error, two deduced types do not match.
S({{42, 42}, std::pair{1,2},std::pair{(char)2,3}});
// Fine, std::pair<int,int> is deduced, others fail but can be converted to it.
S({{42, 42}, std::pair{1,2},{(char)2,3}, {(float)2,3}});
Another ways is to simply specify the T
by hand, then there is no need for any deduction, only matching of the arguments:
S<int>({{42, 42}, {1,2},{2,3}});
I am not quite sure why the rules are as they are, maybe there is some catch with presence of more template arguments. Personally, looking at this now, I feel there could be more elaborate foo
that inherits the inner list signature like:
template<typename T>
void foo(std::pair<T,int>);
and then passes the T
back.
Upvotes: 5