Android源码中有一些经常会遇到的c++基础的内容,笔者对std::move函数进行简单熟悉。

C++ std::move

开篇三连问

  1. 为什么会引入std::move这个函数
  2. 另外什么是左值,什么是右值
  3. 有哪些是右值引用参数的函数

0前言

 1class DataClass {
 2public:
 3    DataClass(int size) : size(size) {
 4        data = new char[size];
 5        memset(data, 0, size);
 6    }
 7
 8    void setData(const char* string)
 9    {
10        std::cout<< "Data len " << strlen(data) << std::endl;
11        if(!strlen(data))
12        {
13            memmove(data, string, size);
14        }
15        std::cout<< "Data string " << data << std::endl;
16    }
17
18    // 深拷贝构造
19    DataClass(const DataClass& temp_array) {
20        std::cout<< " DataClass deep copy " << temp_array.data << std::endl;
21        size = temp_array.size;
22        data = new char[size];
23        for (int i = 0; i < size; i ++) {
24            data[i] = temp_array.data[i];
25        }
26    }
27
28    // 深拷贝赋值
29    DataClass& operator=(const DataClass& temp_array) {
30        std::cout<< " DataClass deep  operator " << temp_array.data << std::endl;
31        delete[] data;
32
33        size = temp_array.size;
34        data = new char[size];
35        for (int i = 0; i < size; i ++) {
36            data[i] = temp_array.data[i];
37        }
38        return *this;
39    }
40
41    ~DataClass() {
42        std::cout << "start ~DataClass" << std::endl;
43        delete[] data;
44    }
45
46private:
47    char *data;
48    int size;
49};

这是一个数据类,我需要一个数据类的对象直接使用之前的对象内容,这个时候往往需要一次深拷贝,比如拷贝构造函数,或者是运算符拷贝函数等。

1int main()
2{
3    DataClass a(12);
4    a.setData((char*)"Hello, Iron");
5    DataClass b(a);//拷贝构造,深拷贝
6    DataClass c(12);
7    c = a;//运算符拷贝,深拷贝
8    return 0;
9}

如果说需要的一个数据类的对象直接使用之前的对象内容,并且之前的对象可以释放,那么可以采用移动函数。

移动函数可以将之前对象的内容移到新的对象中来。

1//如果使用浅拷贝来作为移动函数,往往会有问题
2DataClass(const DataClass& temp_array) {
3    data = temp_array.data;
4    size = temp_array.size;
5    // 为防止temp_array析构时delete data,提前置空其data 
6    temp_array.data = nullptr;
7}

会提示Variable 'temp_array' declared const here cannot assign to variable 'temp_array' with const-qualified type 'const DataClass &',即不能直接修改传参temp_array。

无法实现!temp_array是个const左值引用,无法被修改,所以temp_array.data_ = nullptr;这行会编译不过。当然函数参数可以改成非const:DataClass(DataClass & temp_array){...},这样也有问题,由于左值引用不能接右值,DataClass a = DataClass(DataClass());这种调用方式就没法用了。

这个时候需要一个右值的引用来解决这个问题。

使用方式

1//左值a,用std::move转化为右值
2DataClass c(std::move(a));

1左值右值

左值是表达式结束后依然存在的持久对象(代表一个在内存中占有确定位置的对象)

右值是表达式结束时不再存在的临时对象(不在内存中占有确定位置的表达式)

便携方法:对表达式取地址,如果能,则为左值,否则为右值

1int val;
2val = 4; // 正确 ①
34 = val; // 错误 ②

运算符=要求等号左边是可修改的左值,4是临时参与运算的值,一般在寄存器上暂存,运算结束后在寄存器上移除该值,故①是对的,②是错的。一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址**。**

左值引用

左值引用的基本语法

1T& TypeName = 左值表达式;

右值引用

右值引用基本语法

1T&& TypeName = 左值表达式;

std::move函数

主要可以将一个左值转换成右值引用,从而可以调用C++11右值引用的拷贝构造函数

剖析std::move源码

转发左值,将左值引用转化成右值引用

1template<typename _Tp>
2constexpr _Tp&&
3forward(typename std::remove_reference<_Tp>::type& __t) noexcept
4{ return static_cast<_Tp&&>(__t); }

转发右值,将右值引用转化成右值引用

1template<typename _Tp>
2constexpr _Tp&&
3forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
4{
5  static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
6        " substituting _Tp is an lvalue reference type");
7  return static_cast<_Tp&&>(__t);
8}

实际上,std::move被广泛用于在STL和自定义类中实现移动语义,避免拷贝,从而提升程序性能

2右值引用参数的函数

在STL的很多容器中,都实现了以右值引用为参数的移动构造函数和移动赋值重载函数。

最常见的是vector数组中的推数据函数,push_backemplace_back,目前更多使用的是emplace_back

参数为左值引用意味着拷贝,为右值引用意味着移动。

 1#include <iostream>
 2#include <vector>
 3
 4int main() {
 5    std::string str1 = "20220828";
 6    std::vector<std::string> v;
 7
 8    v.push_back(str1); // 1.传统方法,copy
 9    std::cout << v.back() << std::endl;
10    v.push_back(std::move(str1)); // 2.调用移动语义的push_back方法,避免拷贝,str1会失去原有值,变成空字符串
11    std::cout << v.back() << std::endl;
12    v.emplace_back(std::move(str1)); // 3.emplace_back效果相同,str1会失去原有值
13    std::cout << v.back() << std::endl;
14    v.emplace_back("20220829"); // 当然可以直接接右值
15    std::cout << v.back() << std::endl;
16}

结果

查看源码可以得知

 1//stl_vector.h
 2//第1个函数的原型,是一个左值引用
 3void push_back(const value_type& __x)
 4{
 5    ...
 6}
 7
 8//第2个函数的原型,是一个右值引用,实际调用的是emplace_back
 9#if __cplusplus >= 201103L
10void push_back(value_type&& __x)
11{
12    emplace_back(std::move(__x));
13}
14
15//stl_bvector.h
16void emplace_back(_Args&&... __args)
17{
18    push_back(bool(__args...));
19    #if __cplusplus > 201402L
20    return back();
21    #endif
22}

因此,可移动对象在<需要拷贝且被拷贝者之后不再被需要>的场景,建议使用std::move触发移动语义,提升性能。

3解答

  • 为什么会引入std::move这个函数

    STL和自定义类中,左值转换成右值引用实现移动语义,避免深拷贝,从而提升程序性能

  • 另外什么是左值,什么是右值

    通俗来讲左值是位于运算符左边,右值是运算符右边的值。

    左值是表达式结束后依然存在的持久对象(代表一个在内存中占有确定位置的对象)

    右值是表达式结束时不再存在的临时对象(不在内存中占有确定位置的表达式)

  • 有哪些是右值引用参数的函数

    STL中常见的函数都有右值引用参数的函数,比如vector里面的emplace_back,unique_ptr

    例如

    1std::unique_ptr<A> ptr_a = std::make_unique<A>();
    2std::unique_ptr<A> ptr_b = std::move(ptr_a); // unique_ptr只有移动赋值重载函数,参数是&& ,只能接右值,因此必须用std::move转换类型
    3std::unique_ptr<A> ptr_b = ptr_a; // 编译不通过