推导指南,initializer_list,以及类型推导过程
Deduction guides, initializer_list, and the type deduction process
考虑 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}};
}
我想了解为什么 initializer_list
这个技巧可以编译。乍一看,{1, 2} 被视为 initializer_list
,但随后 重新解释 作为 pair
的列表初始化。
这里到底发生了什么,一步一步?
它可以编译,因为这就是 class 模板推导指南的工作方式。
演绎指南是该类型的假设构造函数。它们并不真正存在。它们的唯一目的是确定如何推导出 class 模板参数。
进行推导后,实际的 C++ 代码将接管 test
的特定实例。因此,编译器的行为就好像您说的是 test<int> t{{1, 2}};
.
而不是 test t{{1, 2}};
test<int>
有一个采用 pair<int, int>
的构造函数,它可以匹配 braced-init-list 中的值,所以这就是被调用的内容。
做这种事情的部分原因是为了让聚合参与 class 模板参数推导。聚合没有用户提供的构造函数,因此如果推导指南仅限于真正的构造函数,则聚合无法工作。
所以我们得到 class 模板推导指南 std::array
:
template <class T, class... U>
array(T, U...) -> array<T, 1 + sizeof...(U)>;
这允许 std::array arr = {2, 4, 6, 7};
工作。它从指南中推导出模板参数和长度,但由于指南不是构造函数,array
仍然是一个聚合。
根据您的推导指南,我们最终得到等同于:
test<int> t{{1, 2}};
由于列表初始化,第 dcl.init.listp3.7 节说:
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 ]
否则我们有一个non-deduced context
考虑 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}};
}
我想了解为什么 initializer_list
这个技巧可以编译。乍一看,{1, 2} 被视为 initializer_list
,但随后 重新解释 作为 pair
的列表初始化。
这里到底发生了什么,一步一步?
它可以编译,因为这就是 class 模板推导指南的工作方式。
演绎指南是该类型的假设构造函数。它们并不真正存在。它们的唯一目的是确定如何推导出 class 模板参数。
进行推导后,实际的 C++ 代码将接管 test
的特定实例。因此,编译器的行为就好像您说的是 test<int> t{{1, 2}};
.
test t{{1, 2}};
test<int>
有一个采用 pair<int, int>
的构造函数,它可以匹配 braced-init-list 中的值,所以这就是被调用的内容。
做这种事情的部分原因是为了让聚合参与 class 模板参数推导。聚合没有用户提供的构造函数,因此如果推导指南仅限于真正的构造函数,则聚合无法工作。
所以我们得到 class 模板推导指南 std::array
:
template <class T, class... U>
array(T, U...) -> array<T, 1 + sizeof...(U)>;
这允许 std::array arr = {2, 4, 6, 7};
工作。它从指南中推导出模板参数和长度,但由于指南不是构造函数,array
仍然是一个聚合。
根据您的推导指南,我们最终得到等同于:
test<int> t{{1, 2}};
由于列表初始化,第 dcl.init.listp3.7 节说:
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 ]
否则我们有一个non-deduced context