简体   繁体   中英

Deduction guides, initializer_list, and the type deduction process

Consider the following code :

#include <initializer_list>
#include <utility>

template<class T>
struct test
{
    test(const std::pair<T, T> &)
    {}
};

template<class T>
test(std::initializer_list<T>) -> test<T>;

int main()
{
    test t{{1, 2}};
}

I would like to understand why this trick with initializer_list compiles. It looks like at first, {1, 2} is treated as an initializer_list , but then it's re-interpreted as a list-initialization of pair .

What exactly happens here, step-by-step?

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.

With your deduction guide we end up with what is equivalent to:

test<int> t{{1, 2}};

This works due to list initialization, section dcl.init.listp3.7 which says:

Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution ([over.match], [over.match.list]). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed. [ Example:

 struct S { S(std::initializer_list<double>); // #1 S(std::initializer_list<int>); // #2 S(); // #3 // ... }; S s1 = { 1.0, 2.0, 3.0 }; // invoke #1 S s2 = { 1, 2, 3 }; // invoke #2 S s3 = { }; // invoke #3 

— end example ] [ Example:

 struct Map { Map(std::initializer_list<std::pair<std::string,int>>); }; Map ship = {{"Sophie",14}, {"Surprise",28}}; 

— end example ] [ Example:

 struct S { // no initializer-list constructors S(int, double, double); // #1 S(); // #2 // ... }; S s1 = { 1, 2, 3.0 }; // OK: invoke #1 S s2 { 1.0, 2, 3 }; // error: narrowing S s3 { }; // OK: invoke #2 

— end example ]

Otherwise we have a non-deduced context

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM