C++17 类模板参数推导

C++17 类模板参数推导

C++17 引入了类模板参数推导(Class Template Argument Deduction,简称 CTAD)这一特性。它的作用很直接:当我们构造一个类模板对象时,编译器可以根据构造函数的参数自动推导模板参数类型,这样很多场景下就不用再手写 <T> 了。

1. 为什么会有类模板参数推导?

C++17 之前,函数模板可以自动推导类型,但类模板一般不行。比如下面这个简单的 Pair

1
2
3
4
5
6
7
template <typename T1, typename T2>
struct Pair {
T1 first;
T2 second;

Pair(T1 a, T2 b) : first(a), second(b) {}
};

在旧写法中,必须显式写出模板参数:

1
Pair<int, double> p(1, 3.14);

虽然这段代码没有问题,但很多时候模板参数其实已经完全体现在构造函数参数里了,再写一遍就显得有些重复。
有了 CTAD 以后,可以直接写成:

1
Pair p(1, 3.14);

编译器会自动推导出这是一个 Pair<int, double>

2. 基本使用

2.1 自定义类模板

还是以上面的 Pair 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

template <typename T1, typename T2>
struct Pair {
T1 first;
T2 second;

Pair(T1 a, T2 b) : first(a), second(b) {}
};

int main()
{
Pair p(10, 3.14);
cout << p.first << ", " << p.second << endl;
return 0;
}

输出:

1
10, 3.14

这里 p 的真实类型就是 Pair<int, double>

2.2 标准库中的使用

CTAD 在标准库里也很常见,例如:

1
2
3
4
5
6
7
8
9
#include <vector>
#include <string>
#include <utility>

int main()
{
std::pair p(1, std::string("hello")); // 推导为 std::pair<int, std::string>
std::vector vec{1, 2, 3, 4}; // 推导为 std::vector<int>
}

这种写法在代码量比较多的时候会显得更简洁,尤其是模板参数本身比较长的时候,效果更明显。

3. 推导指南

有些场景下,编译器虽然能看到构造参数,但未必能按我们的预期推导出模板参数,这时就需要推导指南(Deduction Guide)。

3.1 一个简单例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <string>
using namespace std;

template <typename T>
struct Box {
T value;

Box(T v) : value(v) {}
};

Box(const char*) -> Box<std::string>;

int main()
{
Box b("hello");
cout << b.value << endl;
return 0;
}

如果没有这一行:

1
Box(const char*) -> Box<std::string>;

那么 Box b("hello"); 推导出来的类型更可能是 Box<const char*>
加入推导指南之后,就可以明确告诉编译器:当构造参数是 const char* 时,我希望它推导成 Box<std::string>

3.2 多参数场景

1
2
3
4
5
6
7
8
9
10
template <typename T1, typename T2>
struct MyData {
T1 first;
T2 second;

MyData(T1 a, T2 b) : first(a), second(b) {}
};

template <typename T>
MyData(T, T) -> MyData<T, T>;

这条推导指南表示:如果两个参数类型相同,那么就把它们都推导成同一种类型。
虽然这个例子中编译器大多也能推导出来,但推导指南在更复杂的构造函数设计里会很有用。

4. 使用时要注意的几点

4.1 不是所有类模板都能自动推导

只有在构造函数信息足够明确时,编译器才有办法推导模板参数。
如果类没有合适的构造函数,或者构造函数里看不出模板参数类型,就没法自动推导。

4.2 花括号初始化要注意元素类型

1
std::vector v1{1, 2, 3};    // std::vector<int>

这种写法很自然,但如果花括号里的元素类型混杂,推导结果可能不是你预期的,甚至会直接编译失败。

4.3 推导出来的类型不一定是“最方便”的类型

比如字符串字面量默认是 const char*,如果你想要的是 std::string,通常就需要自己显式写类型,或者像上面那样提供推导指南。

4.4 可读性仍然要放在前面

虽然 CTAD 很方便,但并不是所有地方都适合省略模板参数。
如果省略之后反而让代码不直观,那还不如老老实实写完整。

5. 一个更贴近日常开发的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <map>
#include <string>
#include <iostream>
using namespace std;

int main()
{
std::pair item(1, string("apple"));
std::map<int, string> data;
data.insert(item);

for (const auto& [key, value] : data) {
cout << key << " -> " << value << endl;
}

return 0;
}

这里 std::pair item(1, string("apple")); 就使用了类模板参数推导。写法上会比 std::pair<int, string> 更轻一点。

总结

C++17 的类模板参数推导本质上是帮我们减少模板参数的重复书写。对于标准库容器、自定义简单模板类,以及一些只看构造参数就能确定类型的场景,这个特性都很实用。
不过在使用时也要注意两点:一是并不是所有模板类都能顺利推导,二是不要为了“省几个字”牺牲代码可读性。写得自然、看得明白,才是最重要的。