一、基本概念 模板参数推导(Template Argument Deduction) 是编译器自动分析函数模板调用时的实参类型,从而确定模板参数的过程。
其核心工作就是当调用一个函数模板时,编译器会尝试将调用时传入的实参类型(A) 与模板定义中的参数类型(P) 进行匹配。
1 2 3 template <typename T>void func (T param) ;func (10 );
二、推导类型 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); func (ci); }
注意,不管原先的变量多复杂(带 const、带 volatile),推导出的 T 永远是“干干净净”的基础类型。
因为值传递(Pass-by-Value) 的本质是拷贝 。当你传入一个 const int 时,函数只是拿到了这个值的一份“复印件”。你在函数里修改这个复印件,根本影响不到原本的那个 const int。所以,编译器认为你函数内部是否保持 const 并不重要,它只关心函数内部能正常处理这个值。
2.1.2 按引用传递(T ¶m) 如果你给参数加了 &,规则就变了。编译器为了维护引用关系,不再执行简单的拷贝,它会“顺藤摸瓜”。
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); func (ci); }
一旦加了引用,编译器就会保留 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]) { cout << "数组长度是: " << N << endl; }int main () { int arr[5 ] = {1 , 2 , 3 , 4 , 5 }; get_size (arr); }
2.1.3 转发引用\通用引用(T &¶m) &&,它在普通函数里和在模板函数里,含义完全不同。
规则如下:
如果你传入的是 左值 (比如 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); func (rx); func (42 ); func (cx);
当 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); func (20 );
2.2 auto 的类型推导 auto的类型推导规则与函数模板完全一致:
1 2 3 4 5 6 7 8 auto x = 42 ; const auto & rx = x; auto && rv = 42 ; auto && rl = x; auto arr = name; auto & arr_ref = name;
重要区别:auto的初始化列表推导
1 2 3 4 5 6 7 8 auto x = {1 , 2 , 3 }; auto y{1 , 2 , 3 }; template <typename T>void func (T param) ;func ({1 , 2 , 3 });
2.3 类模板的类型推导(C++ 17) C++ 17引入了类模板的自动类型推导:
1 2 3 4 5 6 7 8 std::pair<int , double > p1 (42 , 3.14 ) ; std::vector<int > v1{1 , 2 , 3 };std::pair p2 (42 , 3.14 ) ; std::vector v2{1 , 2 , 3 }; std::lock_guard lk (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 m2 ("Hello" ) ;
三、完美转发 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); print (std::forward<T>(v)); print (std::move (v)); std::cout << "======================" << std::endl; }int main (int argc, char * argv[]) { int x = 1 ; testForward (x); testForward (std::move (x)); }
在 testForward 中,虽然参数v是右值类型的,但此时v在内存中已经有了位置,所以v其实是个左值。
对于 testForward(x),这里的本质问题在于,左值右值在函数调用时,都转化成了左值,使得函数转调用时无法判断左值和右值。