c++ 右值引用、移动语义、std::move、完美转发std::forward

  • 笔记:
  • 编译器会自动提供默认构造、默认复制构造、默认移动构造、默认移动赋值构造,但也有例外情况。
  • 因此,c++提供class中默认方法和禁用方法
    <1>. 使用default显示声明默认版本: someClass() = default;
    <2>. 使用delete禁止使用默认版本: someClass(const someClass&) = delete;


1 右值

   右值引用 (Rvalue Referene) 是 C++ 新标准 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它实现了转移语义 (Move Sementics) 和精确传递 (Perfect Forwarding)。它的主要目的有两个方面:

  • 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
  • 能够更简洁明确地定义泛型函数。

1.1 左值与右值的定义

  • 左值:指表达式结束后依然存在的持久化对象(占用了一定内存,且拥有可辨认的地址的对象)
  • 右值:指表达式结束时就不再存在的临时对象

值得一提的是,左值的英文简写为“lvalue”,右值的英文简写为“rvalue”。很多人认为它们分别是"left value"、“right value” 的缩写,其实不然。

  • lvalue 是“loactor value”的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据。
  • rvalue 是 "read value"的缩写,指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)。

1.2 区分左值与右值

  • 通常情况下,判断某个表达式是左值还是右值,最常用的有以下 2 种方法。
  • [1] 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值
int a = 5;
  • 变量 a 就是一个左值,5是临时值,是一个右值。值得一提的是,C++ 中的左值也可以当做右值使用,例如:
int b = 10; 	// b 是一个左值
a = b; 			// a、b 都是左值,只不过将 b 可以当做右值使用
  • [2] 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。
  • 以上面定义的变量 a、b 为例,a 和 b 是变量名,且通过 &a 和 &b 可以获得他们的存储地址,因此 a 和 b 都是左值;
  • 反之,字面量 5、10,它们既没有名称,也无法获取其存储地址(字面量通常存储在寄存器中,或者和代码存储在一起),因此 5、10 都是右值。

1.3 左值引用、右值引用

  • 传统的c++引用被称为左值引用,用法如下:
int i = 10;
int& ii = i;
  • C++ Primer Plus 第6版18.1.9中说到,c++11中增加了右值引用,右值引用关联到右值时,右值被存储到特定位置,右值引用指向该特定位置,也就是说,右值虽然无法获取地址,但是右值引用是可以获取地址的,该地址表示临时对象的存储位置。语法如下:
int&& iii = 10;

int num_a = 10;
int num_b = 20;
int&& num_rvalue = num_a+num_b; // num_rvalue关联到30,即便后续修改num_a or num_b,也不会影响到 num_rvalue
  • 如果写如下代码,定义一个左值引用,将其值置为一个常量值,则会报错:
int& i = 10;
  • 原因很明显,左边是一个左值引用,而右边是一个右值,无法将左值引用绑定到一个右值上。
    但是如果是一个const的左值引用,是可以绑定到右值上的。即如下写法是符合语法规范的:
const int& i = 10;
  • 补充重点1:左值引用和右值引用的相互赋值
int&& r_num = 5;
int&  l_num = r_num;
cout << &r_num << " " << &l_num <<endl;
// 打印处的地址为 0x7ffdf3f7d164 0x7ffdf3f7d164
  • 补充重点2:已命名的右值引用,编译器会认为是个左值
void process_value(int& i) { 
 std::cout << "LValue processed: " << i << std::endl; 
} 
 
void process_value(int&& i) { 
 std::cout << "RValue processed: " << i << std::endl; 
} 
 
void forward_value(int&& i) { 
 process_value(i); 
} 
 
int main() { 
 int a = 0; 
 process_value(a); 
 process_value(1); 
 forward_value(2); // 右值传入,右值引用命名为i,此时编译器会认为是个左值
}
// 打印处的地址为 :
// LValue processed: 0 
// RValue processed: 1 
// LValue processed: 2

总结:

  • 左值引用, 使用 T&, 只能绑定左值
  • 右值引用, 使用 T&&, 只能绑定右值
  • 常量左值, 使用 const T&, 既可以绑定左值又可以绑定右值
  • 已命名的右值引用,编译器会认为是个左值

2 移动语义

  • 在旧的c++中,出现了很多的不必须要的拷贝,因为在某些情况下,对象拷贝完之后就下来就销毁了。新标准引入了移动操作,减少了很多的复制操作,而右值引用正式为了支持移动操作而引入的新的引用类型。
  • 移动语义:实际文件还留在原来的位置,只是修改了关联记录。

试列代码:

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
    
class MyString { 
private: 
    char* _data; 
    size_t   _len;

    void _init_data(const char *s) { 
        _data = new char[_len+1]; 
        memcpy(_data, s, _len); 
        _data[_len] = '\0'; 
    } 

public: 
    MyString() = default;

    MyString(const char* p) { 
        _len = strlen (p); 
        _init_data(p); 
    } 

    // 复制构造函数
    MyString(const MyString& str) { 
        _len = str._len; 
        _init_data(str._data); 
        std::cout << "复制构造函数: " << str._data << std::endl; 
    } 

    // 赋值构造函数
    MyString& operator=(const MyString& str) { 
        if (this != &str) { 
            _len = str._len; 
            _init_data(str._data); 
        } 
        std::cout << "=重载函数: " << str._data << std::endl; 
        return *this; 
    } 

    virtual ~MyString() { 
        if (_data) free(_data); 
    } 
}; 
 
int main() { 
    MyString a; 
    a = MyString("Hello"); 

    std::vector<MyString> vec; 
    vec.push_back(MyString("World")); 
}
  • 运算结果

=重载函数: Hello
复制构造函数: World

  • 这个 string 类已经基本满足我们演示的需要。在 main 函数中,实现了调用拷贝构造函数的操作和拷贝赋值操作符的操作。
  • MyString(“Hello”) 和 MyString(“World”) 都是临时对象,也就是右值。虽然它们是临时的,但程序仍然调用了拷贝构造和拷贝赋值,造成了没有意义的资源申请和释放的操作
  • 如果能够直接使用临时对象已经申请的资源,既能节省资源,有能节省资源申请和释放的时间。这正是定义转移语义的目的。


我们定义转移构造函数 and 转移赋值操作符:

  • 转移构造函数(移赋值操作符)和拷贝构造函数(拷贝赋值操作符)类似,有几点需要注意:
    <1>. 参数(右值)的符号必须是右值引用符号,即“&&”。
    <2>. 参数(右值)不可以是常量,因为我们需要修改右值。
    <3>. 参数(右值)的资源链接(char*地址)和标记必须修改。否则,右值的析构函数就会释放资源。转移到新对象的资源也就无效了。
// 转移构造函数 
MyString(MyString&& str) { 
   std::cout << "转移构造函数 : " << str._data << std::endl; 
   _len = str._len; 
   _data = str._data;  	// 复制 char* 地址
   str._len = 0; 		// 改变右值元素,方便区分
   str._data = NULL; 	// 释放指向原 char* 的地址
}

// 转移赋值操作符
MyString& operator=(MyString&& str) { 
   std::cout << "转移赋值操作符: " << str._data << std::endl; 
   if (this != &str) { 
     _len = str._len; 
     _data = str._data; 
     str._len = 0; 
     str._data = NULL; 
   } 
   return *this; 
}
  • 增加了转移构造函数和转移复制操作符后,我们的程序运行结果为 :

转移赋值操作符: Hello
转移构造函数 : World

  • 由此看出,编译器区分了左值和右值,对右值调用了转移构造函数和转移赋值操作符。节省了资源,提高了程序运行的效率。

  • 有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计转移构造函数和转移赋值函数,以提高应用程序的效率。


3. std::move

  • 如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数,也就是把一个左值引用当做右值引用来使用,怎么做呢?
  • 标准库提供了函数 std::move,它的作用是无论你传给它的是左值还是右值,通过std::move之后都变成了右值。

3.1 std::move 的作用


试列代码:

void ProcessValue(int& i) {
	std::cout << "左引用: " << i << std::endl; 
} 
 
void ProcessValue(int&& i) { 
	std::cout << "右引用: " << i << std::endl; 
} 
 
int main() { 
	int a= 0; 
	ProcessValue(a); 
	ProcessValue(std::move(a)); 
	// 输出:
	// 左引用:0
	// 右引用:0
}

  • std::move在提高 swap 函数的的性能上非常有帮助
// 一般来说,swap函数的通用定义如下:
template <class T> swap(T& a, T& b) { 
    T tmp(a);   	// copy a to tmp 
    a = b;      	// copy b to a 
    b = tmp;    	// copy tmp to b 
}

// 有了 std::move,swap 函数的定义变为:
// 通过 std::move,一个简单的 swap 函数就避免了 3 次不必要的拷贝操作。
template <class T> swap(T& a, T& b) { 
    T tmp(std::move(a)); 	// move a to tmp 
    a = std::move(b);    	// move b to a 
    b = std::move(tmp);  	// move tmp to b 
}

3.2 std::move 的实现

  • 实际上std::move就是一个类型转换器,将左值转换成右值而以。我们来看一下它的实现。
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    return static_case<typename remove_reference<T>::type&&>(t);
}
  • 上述代码typename std::remove_reference<T>::type的含义就是获得去掉引用的参数类型。
  • remove_reference利用模板的自动推导获取到了实参去引用后的类型。现在我们再回过来看move函数,之前无法理解的5行代码现然变成了这样:
int && move(int&& && t){
    return static_case<int&&>(t);
}

// or
int && move(int& && t){
    return static_case<int&&>(t);
}
  • 经上面转换后,我们看这个代码就清晰多了,从中我们可以看到move实际上就是做了一个类型的强制转换。如果你是左值引用就强制转换成右值引用。

4. 模板类型推断、引用折叠

  • <1>. 当左值引用作为参数时:
template<class T> void func(T&) {}
func(num_int)           		// num_int是一个int,模板参数类型T是int
func(Const_num_int)         	// Const_num_int是一个const int,模板参数T是const int
func(5)           				// 错误:传递给一个&参数的实参必须是一个左值
  • <2>. 当函数的参数是const的引用时:
template<class T> void func(const T&) {}
func(num_int)           // num_int是一个int,模板参数类型T是int, 因为非const可以转化为const
func(Const_num_int)		// Const_num_int是一个const int,模板参数T是const int
func(5)   				// const的左值引用可以绑定右值,T是int
  • <3>. 当右值引用和模板结合的时候,就复杂了。T&&并不一定表示右值引用,它可能是个左值引用又可能是个右值引用。例如:
template<typename T>
void func( T&& param){
    /* */
}
int a = 0; 
const int &b = 1; 
func(a); 		// 模板参数类型T是 int& (结合在一起 int& &&), a是左值
func(b); 		// 模板参数类型T是 const int&, b是左值
func(2); 		// 模板参数类型T是 int (结合在一起 int &&), 2是右值
  • 所以最终还是要看T被推导成什么类型,如果T被推导成了string,那么T&&就是string&&,是个右值引用,如果T被推导为string&,就会发生类似string& &&的情况,对于这种情况,c++11增加了引用折叠的规则,总结如下:
  • (1). 所有的右值引用叠加到右值引用上仍然使一个右值引用。
  • (2). 所有的其他引用类型之间的叠加都将变成左值引用。
  • 如上面的string& &&其实就被折叠成了个string &,是一个左值引用。
  • 所以,归纳一下, 传递左值进去,就是左值引用,传递右值进去,就是右值引用。

5. 完美转发-std::forward()

5.1 forward作用

  当我们将一个右值引用传入函数时,他在实参中有了命名,所以继续往下传或者调用其他函数时,根据C++ 标准的定义,这个参数变成了一个左值。那么他永远不会调用接下来函数的右值版本,这可能在一些情况下造成拷贝。为了解决这个问题 C++ 11引入了完美转发

  所谓转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。

  • 第四节已经学习了引用折叠,此处看代码理解 std::forward()
#include <iostream>

template<typename T>
void print(T & t){
    std::cout << "左值" << std::endl;
}

template<typename T>
void print(T && t){
    std::cout << "右值" << std::endl;
}

template<typename T>
void testForward(T && v){
    print(v);
    print(std::forward<T>(v));
    print(std::move(v));
}

int main()
{
    testForward(1);

    std::cout << "======================" << std::endl;

    int x = 1;
    testForward(x);
}
  • 输出结果
左值 // 1变成有命名的右值,编译器当其为左值,调用 print(T & t)
右值	// 使用了std::forward函数,所以不会改变它的右值属性
右值	// std::move会将传入的参数强制转成右值
======================
左值	// x变量是左值
左值	// std::forward函数,保持左值
右值 // std::move函数,强转成右值
  • 看了这个例子,应该已经明白了std::forward,通俗的讲就是,如果原来的值是左值,经std::forward处理后该值还是左值;如果原来的值是右值,经std::forward处理后它还是右值。

5.2 forward实现原理

  • 首先来看看forward实现代码:
  • forward实现了两个模板函数,一个接收左值,另一个接收右值。
template <typename T>
T&& forward(typename std::remove_reference<T>::type& param)
{
    return static_cast<T&&>(param);
}

template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param)
{
    return static_cast<T&&>(param);
}

  • 上述代码typename std::remove_reference<T>::type的含义就是获得去掉引用的参数类型。
  • 所以上面的两上模板函数中,第一个是左值引用模板函数,第二个是右值引用模板函数。
  • 紧接着std::forward模板函数对传入的参数进行强制类型转换,转换的目标类型符合引用折叠规则,因此左值参数最终转换后仍为左值,右值参数最终转成右值。

6. STL中emplace 与 emplace_back

  在C++开发过程中,我们经常会用STL的各种容器,比如vector,map,set等,这些容器极大的方便了我们的开发。在使用这些容器的过程中,我们会大量用到的操作就是插入操作,比如vector的push_back。

  • push_back():通常使用push_back()向容器中加入一个右值元素(临时对象)时,
    <1>. 首先会调用构造函数构造这个临时对象;
    <2>. 然后需要调用拷贝构造函数将这个临时对象放入容器中。
    <3>. 原来的临时变量释放。这样造成的问题就是临时变量申请资源的浪费。

  如果可以在插入的时候直接构造,就只需要构造一次就够了。C++11标准已经有这样的语法可以直接使用了,那就是emplace。vector有两个函数可以使用:emplace,emplace_back。emplace 类似 insert,emplace_back 类似 push_back。

  • emplace(), emplace_back()引入了右值引用,直接在容器的指定位置构造对象,这样就省去了拷贝构造的过程。
template <class... Args>
iterator emplace (const_iterator position, Args&&... args);

template <class... Args>
  void emplace_back (Args&&... args);
  • 以emplace_back()为例:
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
    
class A {
public:
	// 构造函数
    A(int i){
        str = to_string(i);
        cout << "  构造函数" << endl; 
    }
    
    // 拷贝构造函数
    A(const A& other): str(other.str){
        cout << "××拷贝构造" << endl;
    }
    
    ~A(){}
    
public:
    string str;
};
    
int main()
{
    vector<A> vec;
    vec.reserve(10);

    cout << "--------- push_back ---------\n";
    // 调用了3次构造函数,和3次拷贝构造函数,
    for(int i=0; i<3; i++){
        vec.push_back(A(i));
    }

    cout << "--------- emplace_back ---------\n";
    // 调用了3次构造函数, 0次拷贝构造函数
    for(int i=0; i<3; i++){
        vec.emplace_back(i); 
    }

}
  • 输出结果:
--------- push_back ---------
  构造函数
××拷贝构造
  构造函数
××拷贝构造
  构造函数
××拷贝构造
--------- emplace_back ---------
  构造函数
  构造函数
  构造函数

7. 参考文章

[1] http://avdancedu.com/a39d51f9/
[2] https://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/index.html
[3] https://www.jianshu.com/p/d19fc8447eaa
[4] https://www.cnblogs.com/likaiming/p/9045642.html
[5] https://zhuanlan.zhihu.com/p/161039484
[6] https://blog.csdn.net/p942005405/article/details/84764104

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页