C++模板参数推导

一、基本概念

模板参数推导(Template Argument Deduction) 是编译器自动分析函数模板调用时的实参类型,从而确定模板参数的过程。

其核心工作就是当调用一个函数模板时,编译器会尝试将调用时传入的实参类型(A)模板定义中的参数类型(P)进行匹配。

1
2
3
template <typename T>
void func(T param);
func(10); // 编译器看到 10 是 int,推导出 T = int

二、推导类型

2.1 函数模板推导

函数模板是最常见的类型推导场景。

2.1.1 按值传递(T param)

这种情况下,编译器会非常“粗暴”地进行复制。

1
2
3
4
5
6
7
8
9
template <typename T>
void func(T x) {}
int main() {
int i = 5;
const int ci = 10;

func(i); // T 推导为 int
func(ci); // T 推导为 int (const 被丢掉了,因为是拷贝一份,原始的 const 属性不影响函数内)
}

注意,不管原先的变量多复杂(带 const、带 volatile),推导出的 T 永远是“干干净净”的基础类型。

因为值传递(Pass-by-Value)的本质是拷贝。当你传入一个 const int 时,函数只是拿到了这个值的一份“复印件”。你在函数里修改这个复印件,根本影响不到原本的那个 const int。所以,编译器认为你函数内部是否保持 const 并不重要,它只关心函数内部能正常处理这个值。

2.1.2 按引用传递(T &param)

如果你给参数加了 &,规则就变了。编译器为了维护引用关系,不再执行简单的拷贝,它会“顺藤摸瓜”。

1
2
3
4
5
6
7
8
9
template <typename T>
void func(T& x) {}
int main() {
int i = 5;
const int ci = 10;

func(i); // T 推导为 int,函数参数变为 int&
func(ci); // T 推导为 const int,函数参数变为 const int&
}

一旦加了引用,编译器就会保留 const 属性。因为函数内如果修改了它,必须得对原始对象负责。

引用(Pass-by-Reference)的本质是别名(Alias)。编译器为了防止你通过引用去修改原本不应该被修改的变量,所以它必须在推导时把 const 属性“打包”进 T 里面。

”数组退化“问题

1
2
3
4
template <typename T>
void func(T a) { }
int arr[5] = {1, 2, 3, 4, 5};
func(arr);

这里的 T 类型是 int *,也就是说当数组作为参数传递给值传递的模板时,它“丢失”了长度信息,只剩下一个指向首元素的指针。

解决方法:给参数加上引用

1
2
3
4
5
6
7
8
9
10
template <typename T, size_t N>
void get_size(T (&arr)[N]) {
// 这里的 N 会被自动推导为数组的长度!
cout << "数组长度是: " << N << endl;
}

int main() {
int arr[5] = {1, 2, 3, 4, 5};
get_size(arr); // 输出 5
}

2.1.3 转发引用\通用引用(T &&param)

&&,它在普通函数里和在模板函数里,含义完全不同。

规则如下:

  • 如果你传入的是 左值(比如 int x),T 会推导为 int&
  • 如果你传入的是 右值(比如 10),T 会推导为 int
1
2
3
4
5
6
7
8
9
10
11
template<typename T>
void func(T&& param);

int x = 42;
int& rx = x;
const int cx = x;

func(x); // x是左值,T = int&, param = int&(引用折叠)
func(rx); // rx是左值引用,T = int&, param = int&
func(42); // 42是右值,T = int, param = int&&
func(cx); // cx是左值,T = const int&, param = const int&

T 被推导为 int& 后,原本的 T&& 就会变成 int& &&。这在数学上是不允许的,但在 C++ 中,编译器有一套“折叠规则”,称作引用折叠(Reference Collapsing):

  • T& + &T& (左值引用)
  • T& + &&T& (左值引用)
  • T&& + &T& (左值引用)
  • T&& + &&T&& (右值引用)

简单来说:只要有一个 & 在里面,最终结果就是 &。只有全是 && 时,才保留 &&

例:

1
2
3
4
5
6
7
8
9
10
11
template <typename T>
void func(T&& val) { }
int x = 10;
func(x); // 1. x 是左值
// 2. T 被推导为 int&
// 3. 函数参数变成了 int& &&,折叠后变成 int&
// 结论:func 接收的是个左值引用
func(20); // 1. 20 是右值
// 2. T 被推导为 int
// 3. 函数参数变成了 int&&
// 结论:func 接收的是个右值引用

2.2 auto 的类型推导

auto的类型推导规则与函数模板完全一致:

1
2
3
4
5
6
7
8
auto x = 42;              // auto = int, x = int(情况1:按值传递)
const auto& rx = x; // auto = int, rx = const int&(情况2:引用传递)
auto&& rv = 42; // auto = int, rv = int&&(情况3:通用引用)
auto&& rl = x; // auto = int&, rl = int&(引用折叠)

// 数组和函数退化
auto arr = name; // auto = const char*, arr = const char*
auto& arr_ref = name; // auto = const char[6], arr_ref = const char(&)[6]

重要区别:auto的初始化列表推导

1
2
3
4
5
6
7
8
// C++11/14
auto x = {1, 2, 3}; // x = std::initializer_list<int>
auto y{1, 2, 3}; // C++14: std::initializer_list<int>, C++17: 错误

// 函数模板不支持直接推导initializer_list
template<typename T>
void func(T param);
func({1, 2, 3}); // 错误!不能推导initializer_list

2.3 类模板的类型推导(C++ 17)

C++ 17引入了类模板的自动类型推导:

1
2
3
4
5
6
7
8
// C++17之前
std::pair<int, double> p1(42, 3.14);
std::vector<int> v1{1, 2, 3};

// C++17之后(类模板参数推导,CTAD)
std::pair p2(42, 3.14); // std::pair<int, double>
std::vector v2{1, 2, 3}; // std::vector<int>
std::lock_guard lk(mutex); // std::lock_guard<std::mutex>

自定义推导指引:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
class MyClass {
public:
MyClass(T val) : data(val) {}
private:
T data;
};

// 自定义推导指引
MyClass(const char*) -> MyClass<std::string>;

MyClass m1(42); // MyClass<int>
MyClass m2("Hello"); // MyClass<std::string>(通过推导指引)

三、完美转发

3.1 引入

软件工程中有一个经典难题:我想写一个“中转”函数,它负责把参数传给另一个函数,同时还要保证这个参数的“身份”(是左值还是右值)不被改变。

如果你在中间转手,如果不加特殊处理,变量转了一圈后,就全变成“左值”了。这时候就需要 std::forward

3.2 std::forward

直接看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
template<typename T>
void print(T & t){
std::cout << "Left value ref" << std::endl;
}

template<typename T>
void print(T && t){
std::cout << "Right value ref" << std::endl;
}

template<typename T>
void testForward(T && v){
print(v);// v此时已经是个左值了,永远调用左值版本的print
print(std::forward<T>(v)); // 重点
print(std::move(v)); // 永远调用右值版本的print

std::cout << "======================" << std::endl;
}

int main(int argc, char * argv[])
{
int x = 1;
testForward(x); //实参为左值
testForward(std::move(x)); //实参为右值,等价于 testForward(1)
}

/* output
Left value ref
Left value ref
Right value ref
======================
Left value ref
Right value ref
Right value ref
======================
*/

在 testForward 中,虽然参数v是右值类型的,但此时v在内存中已经有了位置,所以v其实是个左值。

对于 testForward(x),这里的本质问题在于,左值右值在函数调用时,都转化成了左值,使得函数转调用时无法判断左值和右值。


C++模板参数推导
http://example.com/2026/05/07/C++模板推导/
作者
Yu xin
发布于
2026年5月7日
许可协议