普通右值引用与模板右值引用的差异解析:为何模板右值引用可绑定左值及C++标准依据探讨
哎,这个问题正好戳中了C里一个特别容易搞混的点——模板里的T&&不是普通的右值引用,它有个专门的名字叫「转发引用(Universal Reference)」,也常被称为万能引用,它的行为完全是由C标准里的模板推导规则和引用折叠机制决定的。
一、先搞懂普通右值引用为啥报错
对于void foo1(int&& i),这里的int&&是纯右值引用,按照C++的规则,它只能绑定到右值——比如临时对象、字面量,或者你用std::move()强制转成右值的左值。当你把左值int i = 3传进去的时候,编译器自然会报错,这完全符合预期,毕竟左值和纯右值引用天生不兼容。
二、模板里的T&&为啥能绑定左值?
而模板函数template<typename T> void foo2(T&& i)里的T&&就不一样了,它是转发引用,会根据你传入的实参类型触发模板参数推导和引用折叠:
- 当你传左值
i进去时,模板参数T会被推导成int&(注意是左值引用类型)。这时候T&&就变成了int& &&,根据C++的引用折叠规则,&和&&碰在一起,最终会折叠成int&——也就是左值引用,自然能绑定左值,所以foo2(i)编译完全没问题。 - 要是你传右值(比如
foo2(3)或者foo2(std::move(i))),T会被推导成int,这时候T&&就是int&&,也就是普通的右值引用,绑定右值当然没毛病。
引用折叠的标准规则
C++标准在[dcl.ref]条款里明确了引用折叠的规则:
T& &→ 折叠成T&T& &&→ 折叠成T&T&& &→ 折叠成T&T&& &&→ 折叠成T&&
说白了就是:只要有一个左值引用参与,最终结果就是左值引用;只有当两个都是右值引用的时候,才会得到右值引用。
模板推导的相关标准条款
而转发引用的推导规则,在[temp.deduct.call]条款里有规定:当函数参数是T&&且T是模板参数时,如果传入的实参是左值A&,那么T会被推导为A&;如果实参是右值A,则T被推导为A。这就是为什么传左值时会触发折叠成左值引用的原因。
三、怎么让模板右值引用只绑定右值?
如果你想让模板的T&&参数只能接受右值,可以用模板元编程工具来限制T的推导结果。比如用std::enable_if结合类型特性:
#include <type_traits> template<typename T> std::enable_if_t<!std::is_lvalue_reference_v<T>, void> foo2(T&& i) { // 只有当T不是左值引用时,这个模板才会被实例化 }
要是你用的是C++20及以后,用概念(Concepts)会更简洁:
#include <concepts> template<std::rvalue_reference T> void foo2(T&& i) { // 只接受右值引用类型的参数 }
这样修改后,再传左值进去,编译器就会因为找不到匹配的模板实例而报错,和普通右值引用的行为就一致了。
内容的提问来源于stack exchange,提问作者iammilind

