move语义和完美转发

 

move语义

值类别(value category)如下:

 

 

 

lvalue:左值,在内存中有地址,可被程序员访问,可以放在赋值运算符左侧,也可以放在赋值运算符右侧,常见的左值有普通变量、字符串字面值“hello”等

xvalue:是个左值,但是可以被当做右值使用,需要显式的std::move将其转换为右值;

prvalue:纯右值,无地址,不可被程序员访问到,比如常数42、true、nullptr、this指针、lambda表达式对象等都是纯右值。 

 

右值引用做形参的时候,在函数体内部被当做左值使用:

1 void func(Object&& obj)
2 {
3     // 函数体内部obj被当作左值 
4 
5 }

 

 

类的move构造函数的两段式写法:

 1 CObject(CObject&& other)
 2 {
 3     // 第一段
 4     this->a = std::move(other.a);
 5     this->b = std::move(other.b);
 6     this->ptr = std::move(other.ptr);    
 7     // 第二段
 8     other.ptr = nullptr;
 9 }
10  

 

类的move operator=函数的四段式写法:

 1 CObject& operator=(CObject&& other)
 2 {
 3     // 第一段
 4     if (this == $other)
 5     {
 6         return *this;
 7     }
 8     // 第二段
 9     delete this->ptr;
10     // 第三段
11     this->a = std::move(other.a);
12     this->b = std::move(other.b);
13     this->ptr = std::move(other.ptr);    
14     // 第四段
15     other.ptr = nullptr;
16 }
17  

 

 

编译器的返回值优化(RVO):当在栈上定义了变量并且该变量类型和返回值类型一致时,编译器会做返回值优化:

//////// case1  返回值优化RVO /////////////////////
// (1)obj在栈上申请,该类型和返回值类型一致
CObject foo()
{
    return CObject(); 
}

// (2)基于以上原因,只会调用一次构造函数
CObject object = foo();


//////// case2  具名返回值优化NRVO /////////////////////
// (1)obj在栈上申请,该类型和返回值类型一致
CObject foo()
{
    CObject obj;
    return obj;
}

// (2)基于以上原因,只会调用一次构造函数
CObject object = foo();

 

注意:
(1)同一个变量只能move一次

(2)类的右值引用类新构造函数(转移构造函数)和operator=(转移赋值运算符)依赖其实现,所以在使用的时候要了解其这两个构造函数的实现细节,以免引起误用。

(3)一个变量在被std::move之后就不应该再使用,除了重新被赋值或者被析构掉。

 

 

 

完美转发

完美转发必须存在推理(也就是必须是模版),只有如下一种存在形式,其他形式都是右值引用而不是完美转发

引用折叠规则

  • 若一个右值引用(即带有&&)参数被一个左值或左值引用初始化,那么引用将折叠为左值引用。(即:T&& & –> T&)
  • 若一个右值引用参数被一个右值初始化,那么引用将折叠为右值引用。(即:T&& && 变成 T&&)。
  • 若一个左值引用参数被一个左值或右值初始化,那么引用不能折叠,仍为左值引用(即:T& & –>T&,T& && –>T&)。
1 template<class T>
2 void foo(T&& value) // 实参传入会引发引用折叠!
3 {
   // (0) 这里的value是个左值(具名的右值引用是左值),而实参可能是左值或右值,因此完美转发就是保留实参的实际类型。
   // (1)传入左值或左值引用时,折叠结果T为T&
// (2)传入右值或右值引用时,折叠结果T为T&&
4 bar(std::forward<T>(value)); 5 }

 

 

demo

 1 #include <iostream>
 2 
 3 class CTest
 4 {
 5 public:
 6         CTest()
 7         {
 8                 std::cout << "CTest::CTest" << std::endl;
 9         }
10 
11         ~CTest()
12         {
13                 std::cout << "CTest::~CTest" << std::endl;
14         }
15 
16         CTest(const CTest& t)
17         {
18                 std::cout << "CTest::const CTest&" << std::endl;
19         }
20         CTest(CTest& t)
21         {
22                 std::cout << "CTest::move CTest&" << std::endl;
23         }
24 
25 };
26 
27 void funcImpl(const CTest& t)
28 {
29         std::cout << "funcImpl left" << std::endl;
30 }
31 void funcImpl(CTest&& t)
32 {
33         std::cout << "funcImpl right" << std::endl;
34 }
35 
36 
37 template <typename T>
38 void func(T&& t)
39 {
      // 这里t为左值(一个具有的右值引用为左值)
40 funcImpl(std::forward<T>(t)); /// 关键的地方!!! 41 } 42 43 int main() 44 { 45 CTest t1; 46 func(t1); // t1为左值,左值与T&&的折叠结果T为左值引用T& 47 48 std::cout << std::endl << "---------------------------" << std::endl; 49 CTest t2; 50 func(std::move(t2)); // std::move(t2)为右值,右值与右值引用折叠为T&& 51 52 return 0; 53 }

 

 输出

CTest::CTest
funcImpl left

---------------------------
CTest::CTest
funcImpl right
CTest::~CTest
CTest::~CTest

 

 函数变参模板右值引用

Args可以接受0或多个各种类型的参数,此时可以适配T的各种类型的构造函数。

智能指针MySharedPtr的构造函数只跟T的各种形式有关,与T本身的构造函数无关。

#ifndef __ALGO__
#define __ALGO__

#include "mysharedptr.h"

template <class T, class ... Args>
MySharedPtr<T> make_shared(Args&&... args)
{
    return MySharedPtr<T>(new T(std::forward<Args>(args)...));
} 

#endif

  

 

 

 

 

noexcept

noexcept声明的函数表示函数不会发生异常情况,在某些容器中,如果容器元素对象的move拷贝构造和move构造被声明为noexcept的,那么编译器就会优先使用move版本的拷贝构造和move构造,这样会加快插入速度(默认使用非move版本);

什么时候应该用noexcept?

(1)move拷贝构造

(2)move构造

(3)swap函数

(4)内存分配器的deallocate函数

 

关于C++异常在工程实践里的使用建议:

(1)尽量不使用C++异常,因为写出C++异常安全的代码是困难的

(2)小内存申请默认成功

(3)大内存申请结果判空

(4)标注库异常直接崩溃即可

 

posted on 2022-12-02 15:22  崔好好  阅读(81)  评论(0编辑  收藏  举报

导航