Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

学习自一文读懂C++右值引用和std::move - 知乎 (zhihu.com),原文真的写得超级好!

该文文末还讲解了std::forward,暂时未学习

左右值引用

左值引用

左值引用类型为T&

非const不可以指向右值

int a = 5;
int &ref_a = a; // 左值引用指向左值,编译通过
int &ref_a = 5; // 左值引用指向了右值,会编译失败

const左值引用是可以指向右值的:

const int &ref_a = 5;  // 编译通过

const左值引用不会修改指向值,因此可以指向右值

std::vectorpush_back

void push_back (const value_type& val);

如果没有constvec.push_back(5)这样的代码就无法编译通过了。

右值引用

右值引用类型为T&&

可以指向右值,不能指向左值

可以修改右值

int &&ref_a_right = 5; // ok
 
int a = 5;
int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值
 
ref_a_right = 6; // 右值引用的用途:可以修改右值

右值引用修改右值的实际实现:把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值:

int &&ref_a = 5;
ref_a = 6; 
 
//等同于以下代码:
 
int temp = 5;//只是这个是编译器给准备的一个存储实体,估计这个存储实体是在栈上,和int temp=5;一样是栈上的局部变量,只是我们没有这个变量的名字
int &&ref_a = std::move(temp);
ref_a = 6;

右值引用有办法指向左值吗?

std::move

int a = 5; // a是个左值
int &ref_a_left = a; // 左值引用指向左值
int &&ref_a_right = std::move(a); // 通过std::move将左值类型转化为右值,可以被右值引用指向
 
cout << a; // 打印结果:5

在上边的代码里,看上去是左值a通过std::move移动到了右值ref_a_right中,那是不是a里边就没有值了?并不是,打印出a的值仍然是5。

std::move是一个非常有迷惑性的函数,不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量,但事实上std::move没有做移动,唯一的功能是强制类型转换(把左值强制转化为右值),让右值引用可以指向左值。其实现等同于一个类型转换:**static_cast<T&&>(lvalue)**。

结论

  1. 性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
  2. 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
  3. 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。

std::ref

使用举例见 c++并发编程实践:2.2 传递参数

用于函数接收参数类型为非const左值引用,但是只能拷贝传递的场景下(比如std::thread构造函数中通过附件参数来传参,这些参数都是通过值传递的方式传递的),来包装左值引用

std::refstd::cref是模板函数,返回值是一个std::reference_wrapper对象(该对象有一个数据成员:一个指针),而std::reference_wrapper虽然是一个对象,可是他却能展现出和普通引用类似的效果

头文件#include

编译参数:-std=c++11 -pthread 其中-pthread 库需要

#include <iostream>
#include <thread>
#include <functional> 

void func(int& arg) {
    std::cout<<arg<<std::endl;
}

int main(){
    int a;
    //想把a的引用传入func中,func作为新线程的线程函数
    std::thread th(func, a); //编译器会报错
    std::thread th(func, std::ref(a)); //也是值传递std::ref(a),把std::ref(a)这个对象拷贝到新线程堆栈中(先准备参数,然后是返回地址,然后是旧的栈底指针,然后是栈底...),这个对象中有个指向a的指针
    
    th.join();//等待子线程,因为子线程持有主线程生命周期中的局部变量的引用,所以为了防止未定义行未,主线程得比子线程晚结束
    return 0;
}

常量引用可以绑定到右值

https://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/

Normally, a temporary object lasts only until the end of the full expression in which it appears. However, C++ deliberately specifies that binding a temporary object to a reference to const on the stack lengthens the lifetime of the temporary ++to the lifetime of the reference itself++, and thus avoids what would otherwise be a common dangling-reference error

Note this only applies to stack-based references. It doesn’t work for references that are members of objects

右值引用和std::move的应用场景 移动语义

实现移动语义 移动语义理解

举例

class Array {
public:
    Array(int size) : size_(size) {
        data = new int[size_];
    }
     
    // 深拷贝构造
    Array(const Array& temp_array) {
        size_ = temp_array.size_;
        data_ = new int[size_];
        for (int i = 0; i < size_; i ++) {
            data_[i] = temp_array.data_[i];
        }
    }
     
    // 深拷贝赋值
    Array& operator=(const Array& temp_array) {
        delete[] data_;
 
        size_ = temp_array.size_;
        data_ = new int[size_];
        for (int i = 0; i < size_; i ++) {
            data_[i] = temp_array.data_[i];
        }
    }
 
    ~Array() {
        delete[] data_;
    }
 
public:
    int *data_;
    int size_;
};

该类的拷贝构造函数、赋值运算符重载函数已经通过使用左值引用传参来避免一次多余拷贝了,但是内部实现要深拷贝,无法避免。 这时,有人提出一个想法:是不是可以提供一个移动构造函数把被拷贝者的数据移动过来被拷贝者后边就不要了,这样就可以避免深拷贝了

如:

class Array {
public:
 Array(int size) : size_(size) {
     data = new int[size_];
 }

 // 深拷贝构造
 Array(const Array& temp_array) {
     ...
 }

 // 深拷贝赋值
 Array& operator=(const Array& temp_array) {
     ...
 }

 // 移动构造函数,可以浅拷贝
 Array(const Array& temp_array, bool move) {
     data_ = temp_array.data_;
     size_ = temp_array.size_;
     // 为防止temp_array析构时delete data,提前置空其data_      
     temp_array.data_ = nullptr;
 }


 ~Array() {
     delete [] data_;
 }

public:
 int *data_;
 int size_;
};

这么做有2个问题:

  • 不优雅,表示移动语义还需要一个额外的参数(或者其他方式)。
  • 无法实现!temp_array是个const左值引用,无法被修改,所以temp_array.data_ = nullptr;这行会编译不过。当然函数参数可以改成非const:Array(Array& temp_array, bool move){...},这样也有问题,由于左值引用不能接右值,Array a = Array(Array(), true);这种调用方式就没法用了。

可以发现左值引用真是用的很不爽,右值引用的出现解决了这个问题,

在STL的很多容器中,都实现了以右值引用为参数的移动构造函数移动赋值重载函数,或者其他函数,最常见的如std::vector的push_backemplace_backSTL的函数中,参数为左值引用意味着拷贝,为右值引用意味着移动(这是个设计上的“convention”)

class Array {
public:
    ......
 
    // 优雅
    Array(Array&& temp_array) {
        data_ = temp_array.data_;
        size_ = temp_array.size_;
        // 为防止temp_array析构时delete data,提前置空其data_      
        temp_array.data_ = nullptr;
    }
     
 
public:
    int *data_;
    int size_;
};

如何使用:

int main(){
    Array a;
 
    // 做一些操作
    .....
     
    // 左值a,用std::move转化为右值
    Array b(std::move(a)); 
    // 这里a.data_是nullptr了,原来的a.data_被赋给b.data_
}

移动语义理解

  • 可以这样理解:

    • 拷贝语义:深拷贝
    • 移动语义:浅拷贝 + 把源指针赋值为空
  • 拷贝语义和移动语义的实际应用:类的构造函数和赋值重载函数的实现

    • 可移动不可拷贝:这种类的“拷贝”构造函数都是移动语义的,赋值重载函数都是移动语义的

实例:vector::push_back

int main() {
    std::string str1 = "aacasxs";
    std::vector<std::string> vec;
     
    vec.push_back(str1); // 传统方法,copy
    vec.push_back(std::move(str1)); // 调用移动语义的push_back方法,避免拷贝,str1会失去原有值,变成空字符串
    vec.push_back("axcsddcas"); // 当然可以直接接右值
}
 
// std::vector方法定义
void push_back (const value_type& val);
void push_back (value_type&& val);

加个std::move会调用到移动语义函数,避免了深拷贝。

移动进函数中(函数参数移动构造)

注意1.函数定义的参数类型 2.调用函数用std::move传参

typedef std::map<unsigned, std::set<unsigned>> PatternType;
void print_mapContainer(const PatternType&ptn);
void func(PatternType ptn) // 函数参数类型
{
    std::cout << "[in func:]" << std::endl;
    print_mapContainer(ptn);
}
void test_move_semantics()
{
    PatternType ptn;
    for (int k = 0; k < 3; ++k)
        for (int v = 0; v < 2; ++v)
            ptn[k].insert(k + v);
    print_mapContainer(ptn); // ptn
    func(std::move(ptn)); // 函数调用传参
    std::cout << "[after func call:]" << std::endl;
    print_mapContainer(ptn); // 空了
}

只可移动类型

还有些STL类是move-only的,比如unique_ptr,这种类只有移动构造函数,因此只能移动,不能拷贝:

std::unique_ptr<A> ptr_a = std::make_unique<A>();

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

std::unique_ptr<A> ptr_b = ptr_a; // 编译不通过

建议

除非设计不允许移动,STL类大都支持移动语义函数,即可移动的

另外,编译器默认在用户自定义的classstruct中生成移动语义函数,但前提是用户没有主动定义该类的拷贝语义函数。

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

void func(const T&);//参数类型const T&暗示其实现为拷贝(深拷贝)
void func(T&&);//参数类型T&&暗示其实现为移动(浅拷贝+把源指针赋值为空)

moveable_objecta = moveable_objectb; //调用拷贝赋值重载函数
func(moveable_objectb); //调用参数类型为const T&的func
改为: 
moveable_objecta = std::move(moveable_objectb); //调用移动赋值重载函数
func(std::move(moveable_objectb)); //调用参数类型为T&&的func

std::forward

forward也是仅进行强制类型转换

std::forward(u)有两个参数:T与 u:

  • 当T为左值引用类型时,u将被转换为T类型的左值
  • 否则u将被转换为T类型右值。

举个例子,有main,A,B三个函数,调用关系为:main->A->B,建议先看懂2.3节对左右值引用本身是左值还是右值的讨论再看这里:

void B(int&& ref_r) {
    ref_r = 1;
}
 
// A、B的入参是右值引用
// 有名字的右值引用是左值,因此ref_r是左值
void A(int&& ref_r) {
    B(ref_r);  // 错误,B的入参是右值引用,需要接右值,ref_r是左值,编译失败
     
    B(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
    B(std::forward<int>(ref_r));  // ok,std::forward的T是int类型,属于条件b,因此会把ref_r转为右值
}
 
int main() {
    int a = 5;
    A(std::move(a));
}

例2:

void change2(int&& ref_r) {
    ref_r = 1;
}
 
void change3(int& ref_l) {
    ref_l = 1;
}
 
// change的入参是右值引用
// 有名字的右值引用是 左值,因此ref_r是左值
void change(int&& ref_r) {
    change2(ref_r);  // 错误,change2的入参是右值引用,需要接右值,ref_r是左值,编译失败
     
    change2(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
    change2(std::forward<int &&>(ref_r));  // ok,std::forward的T是右值引用类型(int &&),符合条件b,因此u(ref_r)会被转换为右值,编译通过
     
    change3(ref_r); // ok,change3的入参是左值引用,需要接左值,ref_r是左值,编译通过
    change3(std::forward<int &>(ref_r)); // ok,std::forward的T是左值引用类型(int &),符合条件a,因此u(ref_r)会被转换为左值,编译通过
    // 可见,forward可以把值转换为左值或者右值
}
 
int main() {
    int a = 5;
    change(std::move(a));
}

上边的示例在日常编程中基本不会用到,std::forward最主要运于模版编程的参数转发中

评论