从右值到move

一、为什么需要移动操作?

move(移动)语义是C++11标准新增的特性,其出现的背景主要有亮点原因:

  • 在重新分配内存的过程中,从旧内存将元素拷贝到新内存是不必要的,更好的方式是移动元素来避免拷贝。
  • IO类或unique_ptr这样的类,包含不能被共享的资源。

二、标准库对移动的支持

1. 左值与右值的概念

C++11标准引入了右值引用的概念来支持移动操作。所谓右值引用,就是必须绑定到右值的引用。那么问题来了,右值是什么?
根据《C++ Primer 5th》,不管是左值还是右值,都是C++表达式的属性。可以简单地理解:当一个对象被用作右值的时候,我们使用的对象的值(内容),而当一个对象被用作左值时,我们使用的是它的名字(内存)。左值与右值不能混用:可以用左值代替右值,但右值无法代替左值。注意,这里并不是说右值无法出现在等号的左边。
左值引用的概念我们���经熟悉,右值引用从字面上理解无非就是对右值的引用而已。

int i = 42;
int &r = i; // 正确,r为左值引用
int &&rr = i; // 错误,不能将右值引用绑定到左值
int& r2 = i * 42; // 错误:i * 
const int &r3 = i * 42; // 正确(例外):将const左值引用绑定到右值是合法的
int &&rr2 = i * 42; // 正确,右值引用rr2绑定到右值

2. 标准库move函数

C++11提供了新的标准库函数move来获得绑定到左值上的右值。例如:

int &&rr3 = std::move(rr1); // ok

对rr1使用move操作意味着我们将其从左值转换成一个右值rr3,并且rr1这个引用在其被重新赋值前,我们不会再使用它(除了销毁它之外)。这么做主要是为了支持以下场景:当我们认为对象A不再需要了,想将其内部的资源转移到对象B,我们就将A的资源窃取过来(转为右值引用),直接为B所用。这里B一般需要实现一个以右值为参数的方法来接收A的资源。以标准库vector移动构造函数举个例子:

template <class T>
vector<T>& vector<T>::operator=(vector&& rhs) noexcept {
     destroy_and_recover(begin_, end_, cap_ - begin_);
     begin_ = rhs.begin_;
     end_ = rhs.end_;
     cap_ = rhs.cap_;
     // 设为nullptr可以使rhs的析构不影响this
     rhs.begin_ = nullptr;
     rhs.end_ = nullptr;
     rhs.cap_ = nullptr;
     return *this;
}

可以看到,如果我们决定不再使用rhs,可以以右值的形式传给新vector的移动构造函数,真正的构造过程只需要拷贝三个迭代器begin_、end_、cap_到新的vector,而它们之间的其他迭代器、堆上的数据则不需要再进行任何移动、拷贝。
如果我们以左值为参数,则使用的是拷贝构造函数。大致实现如下:

vector(const vector& rhs) {
     const size_type len = mystl::distance(first, last);
     const size_type init_size = mystl::max(len, static_cast<size_type>(16));
     // 分配空间
     init_space(len, init_size);
     // 拷贝,原地构造
     mystl::uninitialized_copy(first, last, begin_);
}

可以看到,这里先为新的vector初始化了空间,然后通过uninitialized_copy对空间内的元素执行了原地拷贝构造(placement new),显然这种方法的效率低于基于右值的移动构造。

3. move实现

标准库对move的实现如下:

template<typename T>
constexpr typename std::remove_reference<_Tp>::type&&
move(T&& t) noexcept
{return static_cast<typename std::remove_reference<T>::type&&>(t); }

template<typename _Tp>
struct remove_reference
{ typedef _Tp   type; };

template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp   type; };

template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp   type; };

这是一个简单的模板方法,入参是个右值引用T&&,但是我们也可以将一个左值传给move,通过引用折叠(C++ Primer 5th 609)也可以匹配。首先考虑传递给move右值的情况:

string s1("hi"), s2;
s2 = std::move(string("bye!"));

s2 = std::move(string("bye!"))中,

  • 推断出T的类型为string。
  • 匹配到struct remove_reference,其中定义的type为string。
  • move的返回类型是T&&,,即string&&。
    这样,我们就由std::move得到了一个右值引用string&&。
    再考虑传给move左值的情况:
s2 = std::move(s1); // 赋值后s1的值不确定
  • 推断出T的类型为string&(因为此时T&&就通过引用折叠成为了string&)。
  • 因此,remove_reference用string&进行实例化,其type为string。
  • t为string&类型,static_cast将其转为string&&(这是允许的,通常情况下static_cast只能用于合法的类型转换,此处特例)。
  • move的返回类型是T&&,即string&&。
posted @ 2023-04-18 00:18  南小小小小乔  阅读(32)  评论(0)    收藏  举报