类模板参数推导(CTAD)C++17
从C++17开始,当从类模板实例化对象时,编译器可以从对象初始化器的类型中推导出模板类型(这被称为类模板参数推导,简称CTAD)。例如:
#include <utility> // 用于 std::pair
int main()
{
std::pair<int, int> p1{ 1, 2 }; // 显式指定类模板 std::pair<int, int>(C++11及以后版本)
std::pair p2{ 1, 2 }; // 使用CTAD从初始化器推导出 std::pair<int, int>(C++17)
return 0;
}
只有在没有模板参数列表的情况下,才会执行CTAD。因此,以下两种情况都是错误的:
#include <utility> // 用于 std::pair
int main()
{
std::pair<> p1 { 1, 2 }; // 错误:模板参数太少,未能推导出两个参数
std::pair<int> p2 { 3, 4 }; // 错误:模板参数太少,未能推导出第二个参数
return 0;
}
作者注
本网站上的许多后续课程都使用了CTAD。如果你使用C++14标准(或更早版本)编译这些示例,你会遇到关于缺少模板参数的错误。你需要显式添加这些参数,以便示例能够编译。
由于CTAD是一种类型推导形式,我们可以通过使用字面量后缀来改变推导出的类型:
#include <utility> // 用于 std::pair
int main()
{
std::pair p1 { 3.4f, 5.6f }; // 推导为 pair<float, float>
std::pair p2 { 1u, 2u }; // 推导为 pair<unsigned int, unsigned int>
return 0;
}
模板参数推导指南 C++17
在大多数情况下,CTAD可以直接使用。然而,在某些情况下,编译器可能需要一些额外的帮助来正确推导模板参数。
你可能会惊讶地发现,以下程序(与上面使用std::pair
的示例几乎相同)在C++17中无法编译(但在C++20中可以):
// 定义我们自己的 Pair 类型
template <typename T, typename U>
struct Pair
{
T first{};
U second{};
};
int main()
{
Pair<int, int> p1{ 1, 2 }; // 好的:我们显式指定了模板参数
Pair p2{ 1, 2 }; // 在C++17中编译错误(C++20中可以)
return 0;
}
如果你在C++17中编译这个程序,你可能会看到关于“类模板参数推导失败”或“无法推导模板参数”或“没有可行的构造函数或推导指南”的错误。这是因为在C++17中,CTAD不知道如何为聚合类模板推导模板参数。为了解决这个问题,我们可以为编译器提供一个推导指南,告诉编译器如何为给定的类模板推导模板参数。
以下是带有推导指南的相同程序:
template <typename T, typename U>
struct Pair
{
T first{};
U second{};
};
// 这是我们的Pair的推导指南(仅在C++17中需要)
// 用类型为T和U的参数初始化的Pair对象应该推导为Pair<T, U>
template <typename T, typename U>
Pair(T, U) -> Pair<T, U>;
int main()
{
Pair<int, int> p1{ 1, 2 }; // 显式指定类模板 Pair<int, int>(C++11及以后版本)
Pair p2{ 1, 2 }; // 使用CTAD从初始化器推导出 Pair<int, int>(C++17)
return 0;
}
这个示例应该能够在C++17下编译。
我们的Pair类的推导指南非常简单,但让我们更仔细地看看它是如何工作的。
// 这是我们的Pair的推导指南(仅在C++17中需要)
// 用类型为T和U的参数初始化的Pair对象应该推导为Pair<T, U>
template <typename T, typename U>
Pair(T, U) -> Pair<T, U>;
首先,我们使用与我们的Pair类相同的模板类型定义。这是有意义的,因为如果我们的推导指南要告诉编译器如何为Pair<T, U>
推导类型,我们需要定义T和U是什么(模板类型)。其次,在箭头的右侧,我们有我们帮助编译器推导的类型。在这个例子中,我们希望编译器能够为Pair<T, U>
类型的对象推导模板参数,所以我们在这里写下了它。最后,在箭头的左侧,我们告诉编译器要寻找什么样的声明。在这个例子中,我们告诉它寻找一个名为Pair的对象的声明,它有两个参数(一个类型为T,另一个类型为U)。我们也可以写成Pair(T t, U u)
(其中t和u是参数的名称,但由于我们不使用t和u,我们不需要给它们命名)。
将所有内容放在一起,我们告诉编译器,如果它看到一个带有两个参数(分别是类型T和U)的Pair的声明,它应该将类型推导为Pair<T, U>
。
因此,当编译器在我们的程序中看到Pair p2{ 1, 2 };
的定义时,它会说:“哦,这是一个Pair的声明,有两个类型为int
和int
的参数,所以根据推导指南,我应该将其推导为Pair<int, int>
”。
以下是一个类似的单模板类型参数的Pair的示例:
template <typename T>
struct Pair
{
T first{};
T second{};
};
// 这是我们的Pair的推导指南(仅在C++17中需要)
// 用类型为T和T的参数初始化的Pair对象应该推导为Pair<T>
template <typename T>
Pair(T, T) -> Pair<T>;
int main()
{
Pair<int> p1{ 1, 2 }; // 显式指定类模板 Pair<int>(C++11及以后版本)
Pair p2{ 1, 2 }; // 使用CTAD从初始化器推导出 Pair<int>(C++17)
return 0;
}
在这个例子中,我们的推导指南将Pair(T, T)
(一个有两个类型为T的参数的Pair)映射到Pair<T>
。
提示
C++20增加了为聚合类型自动生成推导指南的能力,因此只有在需要C++17兼容性时才需要提供推导指南。
因此,没有推导指南的Pair版本应该能够在C++20下编译。
std::pair
(以及其他标准库模板类型)带有预定义的推导指南,这就是为什么上面使用std::pair
的示例能够在C++17下编译,而无需我们自己提供推导指南。
对于高级读者
非聚合类型在C++17中不需要推导指南,因为构造函数的存在起到了相同的作用。
具有默认值的类型模板参数
就像函数参数可以有默认参数一样,模板参数也可以被赋予默认值。当模板参数没有显式指定且无法推导时,将使用这些默认值。
以下是对上面的Pair<T, U>
类模板程序的修改,将类型模板参数T和U默认为int
类型:
template <typename T=int, typename U=int> // 将T和U默认为int类型
struct Pair
{
T first{};
U second{};
};
template <typename T, typename U>
Pair(T, U) -> Pair<T, U>;
int main()
{
Pair<int, int> p1{ 1, 2 }; // 显式指定类模板 Pair<int, int>(C++11及以后版本)
Pair p2{ 1, 2 }; // 使用CTAD从初始化器推导出 Pair<int, int>(C++17)
Pair p3; // 使用默认的 Pair<int, int>
return 0;
}
我们的p3
的定义没有显式指定类型模板参数的类型,也没有可以从中推导这些类型的初始化器。因此,编译器将使用默认指定的类型,这意味着p3
将是Pair<int, int>
类型。
CTAD不适用于非静态成员初始化
在使用非静态成员初始化初始化类类型成员时,CTAD在这种情况下无法工作。所有模板参数必须显式指定:
#include <utility> // 用于 std::pair
struct foo
{
std::