Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Template argument deduction for implicit pair

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?

like image 350
αλεχολυτ Avatar asked Nov 30 '20 10:11

αλεχολυτ


People also ask

What is template argument deduction?

Template argument deduction is used when selecting user-defined conversion function template arguments. A is the type that is required as the result of the conversion. P is the return type of the conversion function template.

What is type deduction C++?

Type inference or deduction refers to the automatic detection of the data type of an expression in a programming language. It is a feature present in some strongly statically typed languages. In C++, the auto keyword(added in C++ 11) is used for automatic type deduction.

What is a template argument?

A template parameter is a special kind of parameter that can be used to pass a type as argument: just like regular function parameters can be used to pass values to a function, template parameters allow to pass also types to a function.

How many template arguments are there?

1) A template template parameter with an optional name. 2) A template template parameter with an optional name and a default. 3) A template template parameter pack with an optional name.

What is a template argument deduction?

Template argument deduction is also performed when the name of a class template is used as the type of an object being constructed: Template argument deduction for class templates takes place in declarations and in explicit cast expressions; see class template argument deduction for details.

Can a template parameter be deduced from a default argument?

Type template parameter cannot be deduced from the type of a function default argument: Deduction of template template parameter can use the type used in the template specialization used in the function call: Besides function calls and operator expressions, template argument deduction is used in the following situations:

How does the compiler deduce the missing template arguments?

When possible, the compiler will deduce the missing template arguments from the function arguments. This occurs when a function call is attempted, when an address of a function template is taken, and in some other contexts :

What is the difference between type type and non-type template arguments?

Type template argument cannot be deduced from the type of a non-type template argument: When the value of the argument corresponding to a non-type template parameter P that is declared with a dependent type is deduced from an expression, the template parameters in the type of P are deduced from the type of the value.


1 Answers

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.

like image 121
Quimby Avatar answered Oct 17 '22 16:10

Quimby