Initializer_List and Template Type Deduction

initializer_list and template type deduction

Your first line printme({'a', 'b', 'c'}) is illegal because the template argument T could not be inferred. If you explicitly specify the template argument it will work, e.g. printme<vector<char>>({'a', 'b', 'c'}) or printme<initializer_list<char>>({'a', 'b', 'c'}).

The other ones you listed are legal because the argument has a well-defined type, so the template argument T can be deduced just fine.

Your snippet with auto also works because il is considered to be of type std::initializer_list<char>, and therefore the template argument to printme() can be deduced.


The only "funny" part here is that auto will pick the type std::initializer_list<char> but the template argument will not. This is because § 14.8.2.5/5 of the C++11 standard explicitly states that this is a non-deduced context for a template argument:

A function parameter for which the associated argument is an initializer list (8.5.4) but the parameter does not have std::initializer_list or reference to possibly cv-qualified std::initializer_list type. [Example:

template<class T> void g(T);
g({1,2,3}); // error: no argument deduced for T

— end example ]

However with auto, § 7.1.6.4/6 has explicit support for std::initializer_list<>

if the initializer is a braced-init-list (8.5.4), with std::initializer_list<U>.

Deduction guides, initializer_list, and the type deduction process

It compiles because that's how class template deduction guides work.

Deduction guides are hypothetical constructors of the type. They don't really exist. Their only purpose is to determine how to deduce class template parameters.

Once the deduction is made, the actual C++ code takes over with a specific instantion of test. So instead of test t{{1, 2}};, the compiler behaves as if you had said test<int> t{{1, 2}};.

test<int> has a constructor that takes a pair<int, int>, which can match the values in the braced-init-list, so that's what gets called.

This kind of thing was done in part to allow aggregates to participate in class template argument deduction. Aggregates don't have user-provided constructors, so if deduction guides were limited to just real constructors, you couldn't have aggregates work.

So we get to have this class template deduction guide for std::array:

template <class T, class... U>
array(T, U...) -> array<T, 1 + sizeof...(U)>;

This allows std::array arr = {2, 4, 6, 7}; to work. It deduces both the template argument and the length from the guide, but since the guide is not a constructor, array gets to remain an aggregate.

Template argument deduction fails when using braced initializer list

Issue with {/*..*/} is that it has no type, and can mostly only be deduced as std::initializer_list<T> or T[N].

So following would allow desired syntax:

template <typename component>
offset<component> perpendicular(component const (&o)[2]) {
return offset{o[1], -o[0]};
// return perpendicular(offset{o[0], o[1]});
}

Demo

std::initializer_list as template argument for constructor

why is the initializer_list not deduced by the templated constructor?

The reason is that {1,2,3,4,5,6,7,8,9} is just a synctatic construct that doesn't have a type. Therefore, the compiler cannot deduce a type T for this synctatic construct and the first constructor fails.

However, by special Standard rules std::initializer_list<T> (among other things) can be construct from this synctatic construct and T can be deduced to int. Hence the second constructor works.

By constrast with function template argument type deduction, with

auto x = {1,2,3,4,5,6,7,8,9};

the compiler sets the type of x to be std::initializer_list<int>. There are also special Standard rules that says it must be so. Strictly speaking this is not type deduction because, as said above, {1,2,3,4,5,6,7,8,9} doesn't have a type to be deduced. (The only type deduction happening here is T = int in std::initializer_list<T>.) Here the compiler chooses (it doesn't deduce) the type of x to be std::initializer_list<int>. In any case, there's no harm to use the abuse of language of saying that the type of x is deduced to std::initializer_list<int>.

Finally, as DyP said in the comments, what you probably want is inheriting all constructors (not only those taking one argument) from the base container class. You can do this by removing all the constructors that you currently have and add just this line to test:

using container_type::container_type;

Template argument deduction for implicit pair

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.

Deduction guide for brace initializer list

in an environment where standart C++ library is not available

There is no such thing. While freestanding C++ implementations are free to only implement parts of the standard library, there are some components which all valid C++ implementations must provide. std::initializer_list is among these components.

As such, if you have a valid C++11 or higher implementation of C++, then you must have the <initializer_list> header and its contents. This is not optional. If your implementation doesn't provide one, then it is defective.

The reason it is not optional is that the important functionality of std::initializer_list (that is, its generation from a braced-init-list) is a function of the C++ language, not of the library. That is, it is impossible for code outside of the compiler to make the {} grammatical construct become a type that is exactly analogous to how std::initializer_list behaves.

Consider your code:

user<int> sample { 1, 2, 3, 4, 5 };

If you think about it, this ought to mean that a constructor of user<int> will be called that takes 5 parameters. That's what it would mean if user had a constructor of 5 integer parameters, after all. But that's not what you want it to mean, and it wouldn't mean that for vector<int>. Why?

Because C++'s language has a special rule about list initialization that detects the presence of a constructor which takes a std::initializer_list that matches the braced-init-list types, and then creates a std::initializer_list to pass to this constructor. This rule keys off of the presences of a constructor that takes std::initializer_list and no other type.

Your code does not work, not because of a lack of deduction guides, but because your initializer_list type has no special rules as far as the language is concerned.

You cannot recreate this language behavior with a user-defined type. Just as you cannot make typeid return a type other than std::type_info. Just as you cannot make enum class byte: unsigned char{}; have the same behavior as std::byte.

How a function template can deduce the number of times an initializer_list is nested?

You can write two overloaded constexpr function templates to calculate the nested times, with the help of std::enable_if and SFINAE.

// types have not member type value_type
template <typename T, typename = void>
struct has_value_type: std::false_type {};
// types have member type value_type
template <typename T>
struct has_value_type<T, std::void_t<typename T::value_type>> : std::true_type {};

// return nested times as 0 for types without member type value_type
template<typename T>
constexpr std::enable_if_t<!has_value_type<T>::value, size_t> get_nested_times() {
return 0;
}
// return nested times as 1 plus times got on the nested type recursively
template<typename T>
constexpr std::enable_if_t<has_value_type<T>::value, size_t> get_nested_times() {
return 1 + get_nested_times<typename T::value_type>();
}

then you can get the nested times at compile-time as

template<typename List>
auto some_function (const List& list)
{
// N is the number of times the list is nested.
constexpr auto N = get_nested_times<List>();
std::array<size_t, N> arr;
return arr;
}

LIVE



Related Topics



Leave a reply



Submit