章节目录

类模板参数推导(CTAD)

本节阅读量:

类模板参数推导(Class template argument deduction,CTAD)(C++17)

从C++17开始,当从类模板实例化对象时,编译器可以从对象的初始值的类型推断模板参数类型(这称为类模板参数推导或简称CTAD)。例如:

1
2
3
4
5
6
7
8
9
#include <utility> // for 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。因此,以下两项都是错误:

1
2
3
4
5
6
7
8
9
#include <utility> // for std::pair

int main()
{
    std::pair<> p1 { 1, 2 };    // error: 模版参数太少, 2个模版参数都缺少了
    std::pair<int> p2 { 3, 4 }; // error: 模版参数太少, 第二个模版参数缺少

    return 0;
}

由于CTAD是类型推导的一种形式,因此可以使用字面值后缀来更改推导的类型:

1
2
3
4
5
6
7
8
9
#include <utility> // for 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中编译:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 自定义 Pair 类型
template <typename T, typename U>
struct Pair
{
    T first{};
    U second{};
};

int main()
{
    Pair<int, int> p1{ 1, 2 }; // ok: 显示指定模版参数
    Pair p2{ 1, 2 };           // C++17 中编译失败 (C++20 可以编译)

    return 0;
}

如果在C++17中编译它,可能会得到“类模板参数推导失败”或“无法推导模板参数”或“没有可行的构造函数或推导指引”的错误。这是因为在C++17中,CTAD不知道如何推导聚合类模板的模板参数。为了解决这个问题,需要为编译器提供一个演绎指引,告诉编译器如何推导给定类模板的模板参数。

下面是具体的样例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
template <typename T, typename U>
struct Pair
{
    T first{};
    U second{};
};

// 这是我们定义的 Pair 类型的推导指引 (只在 C++17 中必须)
// Pair 对象使用 T 和 U 参数初始化,类型推导为 Pair<T, U>
template <typename T, typename U>
Pair(T, U) -> Pair<T, U>;
    
int main()
{
    Pair<int, int> p1{ 1, 2 }; // 显示声明 std::pair<int, int> (C++11)
    Pair p2{ 1, 2 };           // CTAD,从初始值列表推导 std::pair<int, int> (C++17)

    return 0;
}

这个例子可以在C++17下编译。

Pair类的演绎指引非常简单,让我们仔细看看它是如何工作的。

1
2
3
4
// 这是我们定义的 Pair 类型的推导指引 (只在 C++17 中必须)
// Pair 对象使用 T 和 U 参数初始化,类型推导为 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 t)(其中t和u是参数的名称,但由于不使用t和u,因此不需要为它们命名)。

将它们放在一起,告诉编译器,如果它看到一个具有两个参数的Pair的声明(分别为T和U类型),它应该将类型推断为Pair<T,U>。

因此,当编译器看到 p2{1,2};,它会说,“哦,这是Pair的声明,并且有两个int和int类型的参数,因此使用推导指南,应该将其推导为Pair<int,int>”。

下面是采用单个模板类型参数的Pair的类似示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
template <typename T>
struct Pair
{
    T first{};
    T second{};
};

// 这是我们定义的 Pair 类型的推导指引 (只在 C++17 中必须)
// Pair 对象使用 T 和 T 参数初始化,类型推导为 Pair<T>
template <typename T>
Pair(T, T) -> Pair<T>;

int main()
{
    Pair<int> p1{ 1, 2 }; // 显示声明 std::pair<int> (C++11)
    Pair p2{ 1, 2 };      // CTAD,从初始值列表推导 std::pair<int> (C++17)

    return 0;
}

在这种情况下,我们的推导指南将Pair(T, T)(具有两个类型T作为参数的Pair)映射到Pair<T>。


模板参数默认值

就像函数参数可以有默认参数一样,模板参数也可以给定默认值。当模板参数未显式指定且无法推导时,将使用这些参数。

下面是对上面的Pair<T,U>类模板程序的修改,模板类型参数T和U默认为类型int:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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 }; // 显示声明 std::pair<int, int> (C++11)
    Pair p2{ 1, 2 };           // CTAD,从初始值列表推导 std::pair<int, int> (C++17)

    Pair p3;                   // 使用默认类型 Pair<int, int>

    return 0;
}

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


CTAD不适用于非静态成员初始化

为非静态成员设置默认值时,CTAD在此上下文中不起作用。必须显式指定所有模板参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <utility> // for std::pair

struct Foo
{
    std::pair<int, int> p1{ 1, 2 }; // ok, 模板参数显示指定
    std::pair p2{ 1, 2 };           // 编译失败, CTAD 在这里不会生效
};

int main()
{
    std::pair p3{ 1, 2 };           // ok, CTAD 可以在这里生效
    return 0;
}

CTAD不在函数参数的类型上生效

CTAD代表类模板参数推导,它将只推导给定实际参数的值的情况。

因此,CTAD不能用于函数参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>
#include <utility>

void print(std::pair p) // 编译失败, CTAD 在这里不会生效
{
    std::cout << p.first << ' ' << p.second << '\n';
}

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

    return 0;
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#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 推导 std::pair<int, int>
    print(p);

    return 0;
}

13.10 类模板

上一节

13.12 模板别名

下一节