关于c++:C右值引用与移动语义

4次阅读

共计 2771 个字符,预计需要花费 7 分钟才能阅读完成。

一文看懂 C ++ 右值援用和挪动语义

目录

  • 背景
  • 什么是右值援用
  • 为什么须要右值援用
  • 挪动结构
  • move 的原理
  • move 的利用场景
  • 右值援用注意事项
  • 总结

背景

C++11引入了 右值援用 ,它也是C++11 最重要的新个性之一。起因在于它解决了 C++ 的一大历史遗留问题,即 打消 了很多场景下的 不必要的额定开销。即便你的代码中并不间接应用右值援用,也能够通过规范库,间接地从这一个性中收益。为了更好地了解该个性带来的优化,以及帮忙咱们实现更高效的程序,咱们有必要理解一下无关右值援用的意义。

什么是右值援用

右值

在引入右值的概念前,咱们无妨先看看左值。一句话加以概括:左值就是等号右边的值;同理,右值 也就是 等号左边的值。举个例子:int a = 2;

这里的 a 是等号右边,能够通过取址符 & 来获取地址,所以是一个左值。而 5 在等号左边,无奈通过取址符 & 来获取地址,所以只一个右值。

右值援用

左值援用是对于左值的援用或者叫别名。同样地,右值援用 也就是对于 右值 援用 。语法也很简略,就是在左值援用的语法之上在多加一个&,写成 类型 && 右值援用名 = 右值;的模式即可,比方:

int &&a = 5;
a = 6;
string s1 = "hello";
string &&s2 = s1 + s1;
s2 += s1;

上述简略例子,展现了右值援用的根本用法。不过通常状况下,右值援用更多的是被用于解决函数参数。比方:

struct Student {Student(Student &&s);
};

为什么要应用右值援用

C++11 之前,很多 C++ 程序里存在大量的 长期对象,又称无名对象。次要呈现在如下场景:

  • 函数的返回值
  • 用户自定义类型通过一些计算后产生的长期对象
  • 值传递的形参

先说函数的返回值,最常见的类型就是某些返回用户自定义类型的时候,如果没有将其复制,就会产生长期对象,比方:

Student func1();    // 返回一个 Student 对象
...
func1();            // 调用了 func1 创立了一个 Student 对象,然而没有应用,于是编译器创立了一个长期对象来进行存储

而后是某些计算操作后产生的长期对象,比方:

Complex result = c1 + c2 + c3;    // 编译器先计算 c1 + c2 的后果,并产生一个长期对象 temp 来存储后果,而后计算 temp + c3 的后果,而后将后果复制给 result

还有值传递的形式的形参,例如:

void func(Student s);    // 值传递
...
Student stu;
func(stu);    // 这里相当于是做了一次复制操作    Student s(stu);

而且这些长期对象随着生命周期的完结,编译器还会调用一次析构函数。随着这些操作次数的减少,或者当长期变量是个很大的类型时,这无疑会极大进步程序的开销,从而升高程序的效率。

C++11之后,随着右值援用的呈现,能够无效的解决这些问题。通过 move挪动结构 挪动赋值运算符 函数来取得长期对象的所有权,从而防止拷贝带来的额定开销,进步程序效率

挪动结构

咱们都晓得,因为 C++11 之前,如果没有手动申明,编译器会给一个用于自定义类型(包含 classstruct)主动生成的 4 个函数,别离是构造函数,拷贝构造函数,赋值运算符重载函数和析构函数。尽管通过传援用的形式,能够防止对象的复制。然而还是没法防止上述的长期对象的复制。而挪动语义胜利的解决的这个问题。

C++11 之后,编译器主动生成的函数中又新增了 2 个,它们就是 挪动结构 挪动赋值运算符重载 函数,通过它们,咱们能够很好地实现对用户自定义类型的挪动操作。而挪动的实质就是获取长期对象的所有权,而不是通过复制的形式来取得。间接看代码:

class Foo {
public:
    Foo(Foo &&rhs) : ptr_(rhs.ptr_) {delete rhs.ptr_;}
    
    Foo &operator(Foo &&rhs) {if (*this != rhs) {
            ptr_ = rhs.ptr_;
            delete rhs.ptr_;
        }
        return *this;
    }
    
private:
    int *ptr_;
};

Foo 类重载了挪动构造函数和挪动赋值运算重载函数,使得 Foo 取得了挪动的能力,当咱们在面对产生长期的对象的时候,编译器就会依据传入的参数是左值还是右值来抉择调用拷贝还是挪动。如果是右值,就调用挪动结构或挪动赋值运算符函数。当 Foo 是一个很大的对象时候,就会极大的升高开销,进步程序效率。

move 的利用场景

通过上述例子,咱们能够看到挪动并不是说齐全没有开销,甚至有的时候开销并不一定比拷贝低,具体还是要看长期对象的大小和类型决定,例如:

vector<vector<int>> func() {
    vector<vector<int>> result;
    for (...) {
        vector<int> temp;
        ...
        temp.emplace_back(move(5));            // 没必要,间接传就行了
        ...
        result.emplace_back(move(temp));    // ok,挪动代替拷贝操作,进步了效率
    }
    return result;
}

STL的大部分组件都反对挪动语义,比方 vectorstring 等即能够通过 move 转换右值后调用挪动构造函数进行挪动操作来防止深拷贝。还有一些类是只容许挪动,不容许拷贝,从而更让设计更合乎逻辑,比方unique_ptr

move 的原理

move函数的源码并不简单:

template<class _Ty> 
inline _CONST_FUN typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT {return (static_cast<typename remove_reference<_Ty>::type&&>(_Arg));
}

咱们能够一眼看到,move 的实现其实就做了一件事,如果是左值,就通过 static_cast 将传进来的参数强转为右值并返回;如果是右值,甚至不必转换,间接返回。

右值挪动的注意事项

  • 和左值挪动一样,都须要间接初始化
  • 右值援用无奈指向左值,除非应用 move 将其转成右值,否则编译报错
  • 当对象是根本类型的时候,没必要调用 move,因为拷贝的开销可能还不如函数调用的开销大,尤其是在循环内的时候,须要认真思考
  • move 并不会肯定真的能挪动,它只是将左值强转成右值,只有当该用户自定义类型重载了挪动结构和挪动运算符重载函数时才会进行挪动操作
  • 古代编译在解决返回值的时候,通常都会进行返回值优化,尤其是规范库的组件,应用 move 来接管返回值反而会减少开销
  • 挪动之后的对象就被析构,所以通常是对一些长期对象,或者不再应用的对象进行挪动操作。如果还要持续应用该对象,就要应用拷贝而不是挪动操作
  • 右值援用变量自身是个左值,如果想要右值援用指向右值援用,须要应用 move 转成右值
  • const 左值援用也能够指向右值,然而无奈进行批改
正文完
 0