C++ ~ 左值、右值、左值引用、右值引用、std::move

C++ “左值、右值、左值引用、右值引用” 这一块的内容多且细节繁琐,很容易混淆。这里记录一下自己的理解,简单直观但不一定完全正确,尤其不一定全面,但求够用。

概述

左值可以取地址、位于等号左边;而右值没法取地址,位于等号右边。

引用只是一个别名,只能在初始化的时候指向一个对象并且终身不可修改指向,所以引用的初始化通常称为绑定。左值引用 & 只能绑定左值,右值引用 && 只能绑定右值,一种特殊情况是 const 常量左值引用可以绑定右值。

引入右值和右值引用,就是为了移动语义,为了减少内存拷贝。函数传左值需要调用拷贝构造函数做深拷贝,函数传左值引用可以减少一次拷贝构造函数的拷贝,但免不了函数内部的赋值拷贝。特定场景下可以使用右值调用移动构造函数做浅拷贝,或使用右值引用把函数内部的赋值拷贝也节省掉,直接修改指针指向偷数据块。对于明显可以这么做的右值就自动调用右值重载、右值引用重载了,对于实际可以这样做的左值可以用 std::move 将其声明成不会在后续使用的右值,来重载右值或右值引用版本的函数。

unique_ptr 做函数参数是一个特殊而典型的场景,它的拷贝构造函数和赋值运算符都不能使用,只有移动构造函数有效,所以 unique_ptr 做函数参数传值时必须用 std::move 声明,来调用 unique_ptr 的移动构造函数转移它维护的真实指针。

右值、右值引用

“左值、右值、左值引用、右值引用”主要是体现在函数参数传递上。

右值本质就是临时变量,只在出现的地方被临时创建出来用一次,后面应该不会被用到,因为讲道理它连名字都没有。而临时变量也是一个完整的对象,也申请并赋值了一块内存,本着物尽其用的原则,反正你马上就要被销毁,后面也不会被用到,所以就把临时变量最值钱的东西 - 申请并赋值的内存数据块 - 直接拿过来使用,即指针直接指向那块内存并把临时变量的指针置空,这样就不必再重新申请一块内存再把临时变量的数据复制到新内存中。

函数参数(也就是形参)可能是左值、左值引用、右值引用,当然不可能是右值,右值没有名字。如果函数参数是左值,实参是右值,那就是调用移动构造函数初始化形参,移动构造函数如果你没有定义编译器会自动生成,它的默认实现和拷贝构造函数的默认实现一样,都是浅拷贝。当然,移动构造函数本来就应该用浅拷贝的方式,拷贝构造函数应该是深拷贝,所以我们通常需要手动实现拷贝构造函数,不需要实现移动构造函数。例子,拷贝构造函数打日志,右值没有用到它。

不同形式的函数形参,传入不同实参时所执行的动作总结在下表。

函数参数(形参) 实参 结果
左值 左值 拷贝构造函数
左值 右值 移动构造函数
左值引用 左值 正常绑定
左值引用 右值 编译报错
右值引用 左值 编译报错,需要 std::move 声明才可
右值引用 右值 正常绑定

典型的场景如下,

    string s1 = "s1";
    string s2 = "s2";
	
    vec_str.push_back(s1);		// void vector::push_back(const value_type &__x)
    vec_str.push_back(s1 + s2);		// void vector::push_back(value_type &&__x)

如果 vector 的 push_back 函数是传值,那么需要首先调用一次拷贝构造函数申请内存用实参构造形参,然后再调用一次赋值运算符将形参的数据拷贝给 vector 数组中的元素(假设数组长度足够),因为形参的生命周期只在函数内在函数调用结束后会释放掉。而实际 vector 的 push_back 函数都是传引用,它的左值引用实现版本,节省了一次拷贝构造函数的调用。而在某些特定场景下还可节省掉内存拷贝,就是当实参确定在后面不会被使用时,就直接把实参的数据块转移给 vector 数组中的元素,让元素的数据指针指向实参的数据块,把实参的数据指针置空,这就是右值引用的版本。string 维护一个 char[] 存储字符串,s1 + s2 做字符串拼接就会创建一个临时 string 对象,它存储着拼接好的字符串,并且作为临时变量在函数调用结束后就被释放不会被后面使用,所以匹配上右值引用版本的 push_back,只有一次指针的修改,完全省去了最耗时的内存申请内存拷贝。

std::move 声明左值为右值

对于明显的右值,比如 vec.push_back(string("aaa"))vec.push_back(str1 + str2) ,当然会调用右值重载 push_back(T&&)。但如果不是右值,又保证在后面没啥用了,想调用右值重载怎么办。这种典型的场景,比如对象创建后需要设置一些参数才能传递到函数。这时,就要显式声明这个对象后面不会用到了,这个左值可以当成右值用,声明的方式就是用 std::move

假设 obj 是一个对象,它既然有名字,就是一个左值。std::move(obj) 通常是 obj 做函数参数时对其修饰,声明 obj 在外面没啥用了,你函数想对它怎么着都可以,尤其是想偷它数据也没问题。但是 obj 对象内维护的数据是否被转移走,是函数内部做的事情,与 std::move 这个声明无关,如果函数即使知道 obj 的数据可以被拿走但依然没有拿,那 obj 依然是完整的。当然,STL 中的实现通常是拿走,来节省一次内存拷贝和释放(obj 的数据不需要释放了)的开销,毕竟右值引用设计出来就是做这个事的。

obj 可以是任何对象,常用的场景有两种,一是容器对象或者说内部维护一块数据块的对象,另一个是智能指针 unique_ptr。

左值是容器对象,比如 string 对象维护一个 char 数组,当调用 vec.push_back(std::move(str)) ,str 内部维护的 char 数组就被 push_back(T&&) 拿走(再强调一下,这是 push_back(T&&) 内部的实现逻辑决定的,与 std::move 不直接相关),str 维护的 char 数组为 NULL,表现出来的就是 str.length() == 0

左值是 unique_ptr,unique_ptr 不能赋值和拷贝,只开放了移动构造函数 unique_ptr(unique_ptr&& __u)。当函数参数传值时,左值 unique_ptr 需要用 std::move 声明为右值否则编译报错,形参调用移动构造函数将实参维护的原始指针转移给自己,实参 == NULL。当函数参数传左值引用时,左值 unique_ptr 可以直接使用,传右值引用时,需要使用 std::move 声明为右值,而函数调用结束后实参是否为 NULL 取决于函数内部的实现。实际使用中,unique_ptr 做函数参数通常传值,保证指针只在一个地方使用,并在函数结束时及时销毁。

下面是左值是 unique_ptr,函数传值、传左值引用、传右值引用的例子。

class AAA{
private:
    int aaa = 5;

public:
    ~AAA() = default;
    void show(){
        cout << "AAA.aaa = "<< aaa << endl;
    }
};


void Test(std::unique_ptr<AAA> pp){
    pp->show();
}


int main(){
    std::unique_ptr<AAA> pp = std::make_unique<AAA>();
//    Test(pp);             // 编译报错,因为 unique_ptr 没有拷贝构造函数,只有移动构造函数
    Test(std::move(pp));    // 正常,且 pp == NULL,因为传值隐式调用了 unique_ptr 的移动构造函数
    // unique_ptr 维护的真实指针被转移给形参,当 Test 函数调用结束时,AAA 对象会随形参 unique_ptr 被释放掉 

    if(pp == nullptr){
        cout << "after call Test,pp is nullptr" << endl;
    }else{
        cout << "after call Test,pp is not nullptr" << endl;
    }
}
class AAA{
private:
    int aaa = 5;

public:
    ~AAA() = default;
    void show(){
        cout << "AAA.aaa = "<< aaa << endl;
    }
};


//void Test(std::unique_ptr<AAA> pp){
//    pp->show();
//}


void Test(std::unique_ptr<AAA>& pp){
    cout << "in Test(std::unique_ptr<AAA>& pp)" << endl;
    pp->show();
}


void Test(std::unique_ptr<AAA>&& pp){
    cout << "in Test(std::unique_ptr<AAA>&& pp)" << endl;
    pp->show();
}

int main(){

    std::unique_ptr<AAA> p1 = std::make_unique<AAA>();
    Test(p1);               // 调用左值引用重载,p1 != NULL
    if(p1 == nullptr){
        cout << "after call Test,p1 is nullptr" << endl;
    }else{
        cout << "after call Test,p1 is not nullptr" << endl;
    }

    std::unique_ptr<AAA> p2 = std::make_unique<AAA>();
    Test(std::move(p2));    // 调用右值引用重载,p2 != NULL,void Test(std::unique_ptr<AAA>&& pp) 函数内没有移动 unique_ptr 的指针
    if(p2 == nullptr){
        cout << "after call Test,p2 is nullptr" << endl;
    }else{
        cout << "after call Test,p2 is not nullptr" << endl;
    }
}

当然,无论函数参数是传左值引用还是传右值引用,函数内部都“可以”或者说“有能力”将参数的数据偷走,函数参数声明为左值引用就是告诉用户我不会偷数据,声明为右值引用就是告诉用户我会偷数据来提高性能,你觉得没问题就告诉我你传给我的参数是可以被拿走数据的,即用 std::move 显式声明为右值。

右值引用的具体案例

优化库 G2O 为优化问题对象 optimizer 创建求解器时,几个类的构造函数参数都是 unique_ptr 传值,所以通常使用下面两种方法来构造求解器。其中推荐使用方式一,因为这些中间对象本身就是为求解器服务的,后续不会单独使用,所以没必要生成一个左值。

    explicit OptimizationAlgorithmLevenberg(std::unique_ptr<Solver> solver);
    /*allocate a block solver ontop of the underlying linear solver. 
    NOTE: The BlockSolver assumes exclusive access to the linear solver 
    and will therefore free the pointer in its destructor.*/
    BlockSolver(std::unique_ptr<LinearSolverType> linearSolver);

    // 方式一,直接使用右值
    auto solver = new g2o::OptimizationAlgorithmLevenberg(
            g2o::make_unique<g2o::BlockSolver_6_3>(
                    g2o::make_unique<g2o::LinearSolverCholmod<g2o::BlockSolver_6_3::PoseMatrixType>>()));

    // 方式二,对左值使用 std::move 声明为右值
    auto linearSolver = g2o::make_unique<g2o::LinearSolverCholmod<g2o::BlockSolver_6_3::PoseMatrixType>>();
    auto blockSolver = g2o::make_unique<g2o::BlockSolver_6_3>(std::move(linearSolver));
    auto solver = new g2o::OptimizationAlgorithmLevenberg(std::move(blockSolver));

cartographer_ros

  // node.h
  Node(const NodeOptions& node_options,
       std::unique_ptr<cartographer::mapping::MapBuilderInterface> map_builder,
       tf2_ros::Buffer* tf_buffer, bool collect_metrics);

  // node_main.cc/void Run()
  auto map_builder =
      cartographer::mapping::CreateMapBuilder(node_options.map_builder_options);
  Node node(node_options, std::move(map_builder), &tf_buffer,
            FLAGS_collect_metrics);
posted @ 2024-04-06 23:59  liuxh_cn  阅读(53)  评论(0编辑  收藏  举报