cppc++11右值引用、转移和完美转发
xingzhu
右值引用
左值是指存储在内存中、有明确存储地址(可取地址)的数据 右值是指可以提供数据值的数据(不可取地址)
右值和左值形式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| int num = 10;
int &a = num;
int &&b = 10; int &&p = b;
const int &c = num; const int &g = 10; const int &g1 = a; int &t = c;
const int &&d = 10;
const int &g2 = d; const int &g3 = b; const int &&e = b; const int &&f = d;
|
右值引用
简单来说就是夺舍,就是把临时对象的空间占据,成为管理者,之前的管理者踢出,指向空 然后夺舍之人管理内存空间数据
首先右值的划分:
- 纯右值:非引用返回的临时变量或对象(一般是通过函数的形式)、运算表达式产生的临时变量、原始字面量和 lambda 表达式等
- 将亡值:与右值引用相关的表达式,比如,
T&&
类型函数的返回值、std::move
的返回值等
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 37
| class Test { public: Test():m_num(new int(100)) { cout << "这是默认构造" << " "; printf("m_num 地址: %p\n", m_num); } Test(const Test& a):m_num(new int(*a.m_num)) { cout << "这是拷贝构造" << endl; } Test(Test &&a):m_num(a.m_num){ a.m_num = nullptr; cout << "这是移动构造" << endl; } ~Test() { cout << "这是析构" << endl; delete m_num; } int *m_num; }; Test getObj() { Test t; return t; } int main() { Test t = getObj(); Test t1; Test t2 = t1; }
|
- 在移动构造和拷贝构造都在的时候,就会判断 = 右边的是否是临时对象,是临时对象就调用移动构造,不是就是拷贝构造
- 编译器对于函数返回临时对象,输出结果如果不是上述分析,而是只有默认构造和一次析构,这是因为编译器做的返回值优化
- 当一个函数返回一个局部对象时,编译器可以直接在调用者提供的内存空间中构造这个对象,而不是先在被调用的函数内构造对象然后再拷贝回调用者,这种优化避免了不必要的对象拷贝
- 虽然编译器做了优化,但是调用过程还需知晓
1 2 3 4 5
| Test &&t1 = getObj(); printf("m_num 地址: %p\n", t1.m_num); Test t2 = getObj(); printf("m_num 地址: %p\n", t2.m_num);
|
- 会发现地址一样,说明提供了移动构造函数的临时对象的右值引用有两种写法,可以直接赋值,也可以右值引用
- 如果没有实现移动构造函数,这两种写法都是拷贝构造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
Test getObj1() { return Test(); } Test &&t3 = getObj1(); printf("m_num 地址: %p\n", t3.m_num);
Test&& getObj2() { return Test(); } Test &&t4 = getObj2();
|
第二种即使没有移动构造函数也能实现,是因为编译器提供默认的移动构造函数 但是如果手动实现了移动构造函数,会优先调用手动实现的
&& 特性
T&&
和 auto&&
都是未定义类型,需要推导, const T&&
一定是右值引用
1 2 3 4 5 6 7 8
| template<typename T> void f(T&& param); void f1(const T&& param); f(10); int x = 10; f(x); f1(x); f1(10);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| int x = 520, y = 1314; auto&& v1 = x; auto&& v2 = 250; decltype(x)&& v3 = y;
int&& a1 = 5; auto&& bb = a1; auto&& bb1 = 5;
int a2 = 5; int &a3 = a2; auto&& cc = a3; auto&& cc1 = a2;
const int& s1 = 100; const int&& s2 = 100; auto&& dd = s1; auto&& ee = s2; const auto&& x = 5;
|
auto
要满足是右值引用,等号右边只能是右值,比如常数,其他的所有引用型都不能推导出 右值引用 —> 左值引用
1 2 3
| int &&a = 10; auto&& b = a; int && b = a;
|
总结
- 左值和右值是独立于他们的类型的,右值引用类型可能是左值也可能是右值
- 编译器会将已命名的右值引用视为左值,将未命名的右值引用视为右值
转移和完美转发
move
使用 std::move()
方法可以将左值转换为右值,使用这个函数并不能移动任何东西,而是和移动构造函数一样都具有移动语义,将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝。
1 2 3 4 5 6
| template<class _Ty> _NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) _NOEXCEPT { return (static_cast<remove_reference_t<_Ty>&&>(_Arg)); }
|
1 2 3 4 5 6 7
| list<string> ls; ls.push_back("hello"); ls.push_back("world");
list<string> ls1 = ls; list<string> ls2 = move(ls); list<string> &&ls3 = move(ls);
|
- 这两种方式都可以
- 第一种方式是
move
先将其转换为右值引用,然后 ls2
调用移动构造函数,将 ls
的所有内容所有权转移给 ls2
- 第二种方式则是直接用右值引用,持有
ls
的全部资源
- 如果你需要一个新的对象来拥有原对象的资源,使用
list<string> ls2 = move(ls);
- 如果你需要一个右值引用来延续原对象的资源(比如为了传递给另一个函数),使用
list<string> &&ls3 = move(ls)
forward
右值引用类型是独立于值的,一个右值引用作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,它就变成一个左值,并不是原来的类型了。如果需要按照参数原来的类型转发到另一个函数,可以使用 C++11 提供的 std::forward()
函数,该函数实现的功能称之为完美转发。
1 2 3 4 5 6
| template <class T> T&& forward (typename remove_reference<T>::type& t) noexcept; template <class T> T&& forward (typename remove_reference<T>::type&& t) noexcept;
std::forward<T>(t);
|
- 当
T 为左值引用类型
时,t
将被转换为 T
类型的 左值
- 当
T 不是左值引用类型
时,t
将被转换为 T
类型的 右值
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
| template<typename T> void printValue(T& t) { cout << "l-value: " << t << endl; }
template<typename T> void printValue(T&& t) { cout << "r-value: " << t << endl; }
template<typename T> void testForward(T && v) { printValue(v); printValue(move(v)); printValue(forward<T>(v)); cout << endl; }
int main() { testForward(520); int num = 1314; testForward(num); testForward(forward<int>(num)); testForward(forward<int&>(num)); testForward(forward<int&&>(num)); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| l-value: 520 r-value: 520 r-value: 520
l-value: 1314 r-value: 1314 l-value: 1314
l-value: 1314 r-value: 1314 r-value: 1314
l-value: 1314 r-value: 1314 l-value: 1314
l-value: 1314 r-value: 1314 r-value: 1314
|
解释 testForward 函数,走一遍流程,以第一个 520 为例
- 首先外部传参传的是一个右值引用,
T&&
接收后,v
变成左值引用
printValue(v)
就进入左值引用的实现体,打印 l-value:
move (v)
转换成了右值引用,打印 r-value:
forward<T>(v)
,由于 T
接收的参数是右值引用,因此返回的 t
也是右值引用,打印 r-value:
说明:参考:https://subingwen.cn/


xingzhu
keep trying!keep doing!believe in yourself!
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 星竹!