类模板参数推导(CTAD)和推导指南

类模板参数推导(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的声明,有两个类型为intint的参数,所以根据推导指南,我应该将其推导为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::

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"