问题
- 临时对象非必要的昂贵的拷贝操作
- 在模板函数中如何按照参数的实际类型进行转发
- 关键字:右值、纯右值、将亡值、universal references、引用折叠、移动语义、move语义、完美转发
- 以下用四条代码来阐述C++的右值引用及其思想
1. 第一行代码
- 上式代码会产生一个左值和纯右值,右值是不具名的,判断左值和右值的办法就是看能否取地址
- 在C++11中所有的值必属于左值、将亡值、纯右值三者之一。比如,非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和lambda表达式等都是纯右值。而将亡值是C++11新增的、与右值引用相关的表达式,比如,将要被移动的对象、T&&函数返回值、std::move返回值和转换为T&&的类型的转换函数的返回值等。
2. 第二行代码
2.1 特点1:重获新生
1 2 3 4 5
| T getVal(){ return T(); } T k = getVal(); T&& k = getVal();
|
- F1:调用一次默认构造、两次拷贝构造(一次函数内到函数外的临时值,一次临时值到k)
- F2:调用一次默认构造、一次拷贝构造(一次函数内到函数外的临时值,并且临时值通过右值引用重获新生)
- 现代编译器进行了优化,可能仅仅调用一次默认构造,但这不是C++标准
- 当然在C++98/03年代,为了相同的目的,可以用常量左值引用这种万能引用:
const T& k = getVal();
,也能达到减少一次拷贝构造的目的,但是k不能再改变了。
2.2 特点2:右值引用“二相性”
- 右值引用独立于左值和右值,即,右值引用类型的变量可能是左值也可能是右值,例如:
- val类型为右值引用,但val本身是左值,所有具名变量都是左值
1 2 3 4 5 6 7
| template<typename T> void f(T&& t){}
f(10);
int x = 10; f(x);
|
2.3 特点3:通用引用(universal references)
- T&& t在发生自动类型推断的时候,它是通用引用类型
- 通用引用是需要初始化的,如果是左值,那就归为左值引用,如果是右值,那就归为右值引用。
1 2 3 4
| int a = 1; auto&& b = a; auto&& c = 10;
|
引用折叠
- 所有的右值引用叠加到右值引用上仍然还是一个右值引用
- 所有的其他引用类型之间的叠加都将变成左值引用
- T& &、T& && 和 T&& & 都会折叠成类型 T&
3. 第三行代码
1
| MyClass(MyClass&& a) : m_val(a.m_val) { a.m_val=nullptr; }
|
- 即,移动构造函数(move constructor),采用浅拷贝的方式,因为某些临时变量如果没有移动构造函数,则会频繁发生拷贝构造(深拷贝),如果对象内部堆空间很大的话,代价会非常大
- 这里介绍move语句,move语句会将一个左值变为一个右值类型。
move(val)
后,并不会对val本身做出改变。类似上面的代码,将move(val)
交给一个构造函数或者一个赋值函数,那么会按照右值类型匹配对应的移动构造函数和移动赋值函数,在移动函数里会将val
的资源指针交给别人,val
自身资源指针指向nullptr
,这个时候val
才会发生改变(你在代码里move(val)
一百次,但是不交给对应函数匹配处理,那么val
就不会有一丁点的变化)。当然,这些操作别人已经实现好了,如果是你自己的类,要自己实现!
4. 第四行代码
1 2 3 4
| template <typename T> void f(T&& val){ foo(std::forward<T>(val)); }
|
4.1 C++11前,调用模板函数的问题
1 2 3 4
| template <typename T> void forwardValue(T&& val){ processValue(val); }
|
- 因此引入了完美转发
std::forward
,他会按照参数的实际类型进行转发 1 2 3 4 5 6 7 8 9 10 11 12
| void processValue(int& a){ cout << "lvalue" << endl; } void processValue(int&& a){ cout << "rvalue" << endl; } template <typename T> void forwardValue(T&& val){ processValue(std::forward<T>(val)); } void Testdelcl(){ int i = 0; forwardValue(i); forwardValue(0); }
|
- T&&是一个通用引用(universal references),可以接受左值或者右值,正是这个特性让他适合作为一个参数的路由,然后再通过std::forward按照参数的实际类型去匹配对应的重载函数,最终实现完美转发。
4.2 进一步体会完美转发
4.2.1 问题
func
函数接收的v2
是引用,但是引用的却是middle
里面的一个局部变量t2
,而非main
中的t2
,应该怎么办? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| template <typename F, typename T1, typename T2> void middle(F f, T1 t1, T2 t2) { f(t1, t2); }
void func(int v1, int& v2) { ++v1; ++v2; }
int main(int argc, char* argv[]) { int i = 0; func(42, i); middle(func, 42, i); }
|
4.2.2 尝试
middle
中t1
类型是int&&
,t2
类型是int& && -> int&
,i
的值也如预期般变化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| template <typename F, typename T1, typename T2> void middle(F f, T1&& t1, T2&& t2) { f(t1, t2); }
void func(int v1, int& v2) { ++v1; ++v2; }
int main(int argc, char* argv[]) { int i = 0; func(42, i); middle(func, 42, i); }
|
- 虽然看似没毛病,但是我们改一下
func
,就会暴露问题,修改func如下: 1 2 3 4
| void func(int&& v1, int& v2) { ++v1; ++v2; }
|
- 修改
func
后再用4.2.2的代码运行就会报错,提示:“无法将一个右值引用绑定到左值上”,why?因为,在main
中42虽然是右值,传入到middle
后t1
的类型也确实是int&&
,但是t1
本身作为具名变量,它是一个左值!而左值是无法与右值进行绑定的。
4.2.3 解决
1 2 3 4
| template <typename F, typename T1, typename T2> void middle(F f, T1&& t1, T2&& t2) { f(std::forward<T1>(t1), std::forward<T2>(t2)); }
|
forward
就是解决以上问题的关键
42
传入middle
后绑定到t1
,此时t1类型是int&&
(其中T1
是int
)。通过forward<T1>(t1) -> forward<int>(t1)
,将返回int&&
成功传给func函数。这里你肯定有疑问,刚才不就是int&&
?你绕一大圈子是不是耍人?其实不然,之前的那个啊叫named rvalue,有名字,传参时被当成左值!现在通过forward
返回的是无名字的真正的右值,从而右值得到了保留!
i
传入middle
后绑定到t2
,此时t2类型为int& &&
(其中T2
是int&
,why?因为T2
如果是int
,那么就变成int&& t2 = i
,会导致右值绑定一个左值从而报错!)经过引用折叠变为int&
.通过forward<T2>(t2) -> forward<int&>(t2)
,将返回一个int& &
,折叠后变为int&
,左值也得到了保留!
4.3 应用:泛型工厂函数
利用forward可以实现一个泛型的工厂函数,这个工厂函数可以创建所有类型的对象。具体实现如下: 1 2 3 4
| template<typename… Args> T* Instance(Args&&… args){ return new T(std::forward<Args >(args)…); }
|
- 这个工厂函数的参数是右值引用类型,内部使用std::forward按照参数的实际类型进行转发,如果参数的实际类型是右值,那么创建的时候会自动匹配移动构造,如果是左值则会匹配拷贝构造。
参考资料