13-14 类模板参数推导(CTAD)与推导指南

类模板参数推导(CTAD)(C++17)

从 C++17 开始,当从类模板实例化对象时,编译器能够根据对象初始化器的类型推导出模板类型(这被称为类模板参数推导class template argument deduction,简称 CTAD)。例如:

#include <utility> // for std::pair

int main()
{
    std::pair<int, int> p1{ 1, 2 }; // explicitly specify class template std::pair<int, int> (C++11 onward)
    std::pair p2{ 1, 2 };           // CTAD used to deduce std::pair<int, int> from the initializers (C++17)

    return 0;
}

image

仅当不存在模板参数列表时才会执行CTAD。因此,以下两种情况均会导致错误:

#include <utility> // for std::pair

int main()
{
    std::pair<> p1 { 1, 2 };    // error: too few template arguments, both arguments not deduced
    std::pair<int> p2 { 3, 4 }; // error: too few template arguments, second argument not deduced

    return 0;
}

image
image

作者注:
本网站后续课程将频繁使用CTAD技术。若您使用C++14标准(或更早版本)编译这些示例,将因缺少模板参数而报错。需在示例中显式添加参数才能通过编译。

由于CTAD属于类型推导机制,我们可通过字面量后缀修改推导出的类型:

#include <utility> // for std::pair

int main()
{
    std::pair p1 { 3.4f, 5.6f }; // deduced to pair<float, float>
    std::pair p2 { 1u, 2u };     // deduced to pair<unsigned int, unsigned int>

    return 0;
}

image


模板参数推导指南 C++17

在大多数情况下,CTAD(模板参数推导)能直接生效。但在某些特殊情况下,编译器可能需要额外提示才能正确推导模板参数。

你可能会惊讶地发现,以下程序(几乎与上述使用std::pair的示例完全相同)在C++17(仅限C++17)中无法编译:

// define our own Pair type
template <typename T, typename U>
struct Pair
{
    T first{};
    U second{};
};

int main()
{
    Pair<int, int> p1{ 1, 2 }; // ok: we're explicitly specifying the template arguments
    Pair p2{ 1, 2 };           // compile error in C++17 (okay in C++20)

    return 0;
}

image

若在C++17环境下编译此代码,很可能会出现“类模板参数推导失败”、“无法推导模板参数”或“无可用构造函数或推导指南”等错误。这是因为在C++17中,CTAD无法推导聚合类模板的模板参数。为解决此问题,我们可以为编译器提供推导指南deduction guide,告知其如何推导给定类模板的模板参数。

以下是添加推导指南后的同款程序:

template <typename T, typename U>
struct Pair
{
    T first{};
    U second{};
};

// Here's a deduction guide for our Pair (needed in C++17 only)
// Pair objects initialized with arguments of type T and U should deduce to Pair<T, U>
template <typename T, typename U>
Pair(T, U) -> Pair<T, U>;

int main()
{
    Pair<int, int> p1{ 1, 2 }; // explicitly specify class template Pair<int, int> (C++11 onward)
    Pair p2{ 1, 2 };           // CTAD used to deduce Pair<int, int> from the initializers (C++17)

    return 0;
}

image

此示例应能在C++17下编译通过。

我们的Pair类的推导规则相当简单,但让我们更深入地了解其工作原理。

// Here's a deduction guide for our Pair (needed in C++17 only)
// Pair objects initialized with arguments of type T and U should deduce to 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,故无需命名)。

综合来看,我们告诉编译器:当遇到声明为 Pair 且带有两个参数(分别是类型 T 和 U)时,应推断其类型为 Pair<T, U>。

因此当编译器在程序中遇到定义 Pair p2{ 1, 2 }; 时,它会判断:“这是 Pair 的声明,且包含两个类型为 int 的参数,根据类型推导指南,应将其推导为 Pair<int, int>”。

以下是接受单一模板类型的 Pair 示例:

template <typename T>
struct Pair
{
    T first{};
    T second{};
};

// Here's a deduction guide for our Pair (needed in C++17 only)
// Pair objects initialized with arguments of type T and T should deduce to Pair<T>
template <typename T>
Pair(T, T) -> Pair<T>;

int main()
{
    Pair<int> p1{ 1, 2 }; // explicitly specify class template Pair<int> (C++11 onward)
    Pair p2{ 1, 2 };      // CTAD used to deduce Pair<int> from the initializers (C++17)

    return 0;
}

在此情况下,我们的推导指南将 Pair(T, T)(一个带有两个类型为 T 的参数的 Pair)映射到 Pair

提示:
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> // default T and U to type 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 }; // explicitly specify class template Pair<int, int> (C++11 onward)
    Pair p2{ 1, 2 };           // CTAD used to deduce Pair<int, int> from the initializers (C++17)

    Pair p3;                   // uses default Pair<int, int>

    return 0;
}

我们对 p3 的定义并未显式指定类型模板参数的类型,也没有可用于推导这些类型的初始化器。因此,编译器将使用默认值中指定的类型,这意味着 p3 的类型将为 Pair<int, int>。


CTAD不支持非静态成员初始化

当使用非静态成员初始化来初始化类型的成员时,CTAD在此上下文中将失效。所有模板参数都必须显式指定:

#include <utility> // for std::pair

struct Foo
{
    std::pair<int, int> p1{ 1, 2 }; // ok, template arguments explicitly specified
    std::pair p2{ 1, 2 };           // compile error, CTAD can't be used in this context
};

int main()
{
    std::pair p3{ 1, 2 };           // ok, CTAD can be used here
    return 0;
}

image


CTAD不适用于函数参数

CTAD代表类模板参数推导(Class Template Argument Deduction),而非类模板参数推导(Class Template Parameter Deduction),因此它仅能推导模板参数的类型,而非模板参数本身。(自己注解:在国内的书中有些地方argument翻译为实参,是调用者; parameter翻译为形参,被调用者 )

因此,CTAD无法应用于函数参数中。

#include <iostream>
#include <utility>

void print(std::pair p) // compile error, CTAD can't be used here
{
    std::cout << p.first << ' ' << p.second << '\n';
}

int main()
{
    std::pair p { 1, 2 }; // p deduced to std::pair<int, int>
    print(p);

    return 0;
}

image
image

在这种情况下,您应该改用模板:

#include <iostream>
#include <utility>

template <typename T, typename U>
void print(std::pair<T, U> p)
{
    std::cout << p.first << ' ' << p.second << '\n';
}

int main()
{
    std::pair p { 1, 2 }; // p deduced to std::pair<int, int>
    print(p);

    return 0;
}
posted @ 2025-12-24 20:43  游翔  阅读(9)  评论(0)    收藏  举报