C++ 通用引用
C++ 通用引用
&&
并不是在所有情况下都代表右值引用。
Widget&& var1 = someWidget; // here, “&&” means rvalue reference
auto&& var2 = var1; // here, “&&” does not mean rvalue reference
template<typename T>
void f(std::vector<T>&& param); // here, “&&” means rvalue reference
template<typename T>
void f(T&& param); // here, “&&”does not mean rvalue reference
右值引用只能绑定到右值,而左值引用往往只能绑定到左值,但被声明为 const 的左值引用可以绑定到右值上。
声明为 &&
的引用有时既可以绑定到右值上,也可以绑定到左值上,称之为通用引用(Universal References)。由于通用引用常常用于完美转发的场景中,因此又叫又叫转发引用。
🔑 如果一个变量或参数,被声明为 T&& ,T 是某个被推导出的类型,那么这个变量或参数是通用引用
和所有引用一样,通用引用也必须被初始化:
- 用左值初始化通用引用,它将变为一个左值引用;
- 用右值初始化通用引用,它将变为一个右值引用
🔑 实践中,可以使用以下法则来区分左值和右值
- 如果你可以对某个表达式取地址,那么它是左值;
- 如果一个表达式的类型是左值引用( T& 或 const T& 等),那么它是左值;
- 否则这个表达式是右值,从概念上来讲,右值代表临时的对象,比如函数的返回值,或通过隐式类型转换创建的值;大多数字面量(比如 10 和 5.3)也是右值
再次考虑之前的代码:
Widget&& var1 = someWidget;
auto&& var2 = var1;
你可以对 var1 取地址,所以 var1 是左值,因此通用引用 var2 被推导为左值引用。
std::vector<int> v;
auto&& val = v[0]; // val becomes an lvalue reference (see below)
由于 v[0]
是左值引用,因此它是左值,通用引用 val 被推导为左值引用。
template<typename T>
void f(T&& param); // “&&” might mean rvalue reference
f(10); // 10 is an rvalue
10 是右值,因此 param 被推导为右值引用。
int x = 10;
f(x); // x is an lvalue
x 是左值,因此 param 被推导为左值引用。
🔑 只有 && 跟类型推导一起出现时,它才是一个通用引用
通用引用必须以 T&&
的形式出现,虽然 std::vector<T>&& param
中 T 的类型会被推导,但它是右值引用:
template<typename T>
void f(std::vector<T>&& param); // “&&” means rvalue reference
即使在前面加上 const 也会让 &&
不再被解释为通用引用:
template<typename T>
void f(const T&& param); // “&&” means rvalue reference
但有时,即使形式为 T&&
的引用也不是通用引用:
template <class T, class Allocator = allocator<T> >
class vector {
public:
...
void push_back(T&& x);
// fully specified parameter type ⇒ no type deduction;
... // && ≡ rvalue reference
};
因为 T 的类型是由 vector<T>
的模板参数 T 决定的,这里并不存在类型推导,因此它不是通用引用。
与 emplace_back 的参数作比较,它的类型独立于 vector<T>
的模板参数 T,是编译器从实参中推导出来的,因此它是通用引用:
template <class T, class Allocator = allocator<T> >
class vector {
public:
...
template <class... Args>
void emplace_back(Args&&... args);
// deduced parameter types ⇒ type deduction;
... // && ≡ universal references
};
引用折叠
模板实参推断
函数参数是通用引用时:
- 传递给它左值,编译器推断 T 类型为实参的左值引用类型
- 传递给它右值,编译器推断 T 类型为实参的原始类型
- 注意,右值并不会被推导为右值引用类型
因此,使用通用引用时,可能间接产生引用的引用(注意,在 C++中直接声明引用的引用是非法的),这种情况下会发生引用折叠:
X& &
、X& &&
、X&& &
都折叠为X&
X&& &&
折叠为X&&
template <typename T> void func(T &&t);
int main() {
int i = 0;
int& ir1 = i;
int&& ir2 = 10;
const int ci = 100;
const int &cir = ci;
func(i); // void func<int &>(int &t)
func(ir1); // void func<int &>(int &t)
func(ir2); // void func<int &>(int &t)
func(std::move(i)); // void func<int>(int &&t)
func(ci); // void func<const int &>(const int &t)
func(cir); // void func<const int &>(const int &t)
}
传 ir1 进去,T 被推断为 int &
,所以参数类型为 int & &&
,根据引用折叠的规则,它被折叠为 int &
,因此,通用引用可以通过类型推导和引用折叠机制变为左值引用。
如果传给通用引用的初始值本身是左值引用或右值引用,则它的引用性质被忽略,因此传给 T&&
参数一个 int&
或 int&&
,都会被看作 int,ir1 和 ir2 都是左值,所以 T 都会被推导为 int &
。
总结一下,类型推导机制区分实参是左值还是右值,引用折叠机制来将推导出的引用变成一个“正常”的引用,因此,可以将通用引用理解为引用折叠上下文中的右值引用。
将左值转换为右值的常规方法是调用 std::move
:
Widget var2 = std::move(var1); // equivalent to above
事实上与使用 static_cast<Widget&&>(var1)
做转换的效果是相同的。
我们以 std::move
的实现为例,讲解类型推导的过程:
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
对于表达式 std::move(string("hello"))
- 传进来的是右值,所以 T 被推导为 string
- t 的类型为
string &&
remove_reference<T>::type&&
为string &&
对于表达式 s1 = string("world"); std::move(s1);
- 传进来的是左值,所以 T 被推导为
string&
- t 的类型为
string& &&
,被折叠为string&
remove_reference<T>::type&&
为string &&
所以无论实参的类型是左值还是右值,std::move
都会将它转换为右值
auto
引用折叠也会发生在用 auto&&
声明变量的上下文中,auto 的推导规则和模板实参推导基本相同(仅在列表初始化的上下文中稍有区别)。
Widget&& var1 = someWidget; // var1 is of type Widget&& (no use of auto here)
auto&& var2 = var1; // var2 is of type Widget& (see below)
var1 是左值,因此推断出的类型为 Widget&
,根据引用折叠,初始化语句相当于:
Widget& var2 = var1; // var2 is of type Widget&
typedef
引用折叠可能发生的第三个场景是 typedef。
template<typename T>
class Widget {
typedef T& LvalueRefType;
...
};
当我们这样使用该模板时:
Widget<int&> w;
typedef 语句变为:
typedef int& & LvalueRefType;
此时引用折叠发生,产生代码:
typedef int& LvalueRefType;
我们在引用的场景下使用该 typedef 类型也会发生引用折叠:
void f(Widget<int&>::LvalueRefType&& param);
param 的类型相当于:
void f(int& && param);
发生引用折叠,变为:
void f(int& param);
decltype
最后一个可能发生引用折叠的场景为 decltype,decltype 上应用的类型推导规则不同于模板和 auto,需要注意的是,它的类型推导仅依赖于 decltype 表达式,与初始值无关,因此
Widget w1, w2;
auto&& v1 = w1; // v1 的类型为 Widget&
decltype(w1)&& v2 = w2; // v2 的类型是 Widget&&,而 w2 是左值
// 因此该语句是非法的
通用引用和函数重载
通用引用几乎可以精确匹配任何类型的实参,因此有通用引用重载的情况下,函数匹配规则会变得异常的复杂:
对此,Effective Modern C++ 给我们的建议是不要在通用引用上做函数重载。
🔑 Effective Modern C++, Item 26: Avoid overloading on universal references (Scott Meyers)
参考资料
- [1] https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers
- [2] Effective Modern C++
本文来自博客园,作者:路过的摸鱼侠,转载请注明原文链接:https://www.cnblogs.com/ljx-null/p/15940982.html