右值引用和移动语义

简介: 右值引用和移动语义

什么是左值?什么是右值?

通俗来讲,可以出现在赋值语句左侧的,为左值;只能出现在赋值语句右侧的,为右值。

左值与右值的本质区别在于:左值能取地址,但右值不能

本文主要通过三个场景 —— 与 移动构造、移动赋值、完美转发 有关,讲解右值引用在实际场景中的作用及其相关的知识。

补充: 右值通常也被形象的称为“将亡值”,代指赋值重载和拷贝构造过程中产生的临时对象

为了观察现象,我们要用到“自己搭的轮子”:

namespace MyTest
{
  class string
  {
  public:
    typedef char* iterator;
    iterator begin()
    {
      return _str;
    }
    iterator end()
    {
      return _str + _size;
    }
    string(const char* str = "")
      :_size(strlen(str))
      , _capacity(_size)
    {
      // cout << "string(const char* str)" << endl;
      _str = new char[_capacity + 1];
      strcpy(_str, str);
    }
    // s1.swap(s2)
    void swap(string& s)
    {
      ::swap(_str, s._str);
      ::swap(_size, s._size);
      ::swap(_capacity, s._capacity);
    }
    // 拷贝构造 -- 左值
    string(const string& s)
      :_str(nullptr)
    {
      cout << "string(const string& s) -- 深拷贝" << endl;
      _str = new char[s._capacity + 1];
      strcpy(_str, s._str);
      _size = s._size;
      _capacity = s._capacity;
    }
    // 拷贝赋值
    string& operator=(const string& s)
    {
      cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
      string tmp(s);
      swap(tmp);
      return *this;
    }
    ~string()
    {
      delete[] _str;
      _str = nullptr;
    }
    void reserve(size_t n)
    {
      if (n > _capacity)
      {
        char* tmp = new char[n + 1];
        strcpy(tmp, _str);
        delete[] _str;
        _str = tmp;
        _capacity = n;
      }
    }
    string& operator+=(char ch)
    {
      push_back(ch);
      return *this;
    }
    void push_back(char ch)
    {
      if (_size >= _capacity)
      {
        size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
        reserve(newcapacity);
      }
      _str[_size] = ch;
      ++_size;
      _str[_size] = '\0';
    }
    const char* c_str() const
    {
      return _str;
    }
  private:
    char* _str = nullptr;
    size_t _size = 0;
    size_t _capacity = 0; // 不包含最后做标识的\0
  };
  string to_string(int value)
  {
    bool flag = true;
    if (value < 0)
    {
      flag = false;
      value = 0 - value;
    }
    string str;
    while (value > 0)
    {
      int x = value % 10;
      value /= 10;
      str += ('0' + x);
    }
    if (flag == false)
    {
      str += '-';
    }
    std::reverse(str.begin(), str.end());
    return str;
  }
}

1. 移动构造

1.1 原理讲解
int main()
{
    MyTest::string s1 = MyTest::to_string(1234);
    
    return 0;
}

要分别从 C++11 前后编译器优化前后 四个维度讨论 MyTest::string s1 = MyTest::to_string(1234); 的发生过程。

观察上图的右下角:很明显,str 是左值,在没有 move 的情况下,为什么可以直接移动构造

实际上,编译器已经做过优化了 —— 将传值返回函数的返回值隐式地 move 过了

1.2 移动构造实现
// 移动构造
    string(string&& s)
    {
        cout << "string(string&& s) -- 移动构造" << endl;
        swap(s);
    }

2. 移动赋值

2.1 原理
int main()
{
  MyTest::string s1;
  s1 = MyTest::to_string(1234);
  return 0;
}

2.2 移动赋值实现
// 移动赋值
    string& operator=(string&& s)
    {
        cout << "string& operator=(string&& s) -- 移动赋值" << endl;
        swap(s);
        return *this;
    }

3. 完美转发

3.1 forward 及其用法

move(...) 表达式的结果为右值,但并不会改变 ... 本身的属性 。

forward<T>(x) 返回一个引用表达式,这个引用表达式的类型取决于 x 的原始类型和上下文。

若 x 为右值,该引用表达式具有右值属性

若 x 为左值,该引用表达式具有左值属性

  • 右值的右值引用具有左值属性
void func(const int& x)
{
  cout << "const int&" << endl;
} 
void func(int&& x)
{
  cout << "int&&" << endl;
}
template<class T>
void fun(T&& x)
{
  func(x);// 此处 x 为右值还是左值呢?不妨运行该段代码,试试看
}
int main()
{
  // int&& a = 10;// 10 为右值;a 为 10 的右值引用,因此具有左值属性
  fun(10);
  return 0;
}

做一处修改:

template<class T>
void fun(T&& x)
{
  func(forward<T>(x));// 再运行试试
}

3.2 模板中的 && 万能引用

请记住:万能引用 与 模板 紧密相关!

void func(int& x) { cout << "左值引用" << endl; }
void func(const int& x) { cout << "const 左值引用" << endl; }
void func(int&& x) { cout << "右值引用" << endl; }
void func(const int&& x) { cout << "const 右值引用" << endl; }
template<class T>
void Universal_citation(T&& x) // 万能引用
{ 
    func(forward<T>(x));
}
int main()
{
    int a;
    Universal_citation(a);
    Universal_citation(move(a));
    
    const int b = 10; // b 必须要初始化
    Universal_citation(b);
    Universal_citation(move(b));
    
    return 0;
}

3.3 实际场景

再引入一个“自己造的轮子” —— list :

namespace MyTest
{
  template<class T>
  struct ListNode
  {
    ListNode<T>* _next;
    ListNode<T>* _prev;
    T _data;
    ListNode(const T& x = T())
      :_next(nullptr)
      , _prev(nullptr)
      , _data(x)
    {}
  };
  template<class T, class Ref, class Ptr>
  struct __list_iterator
  {
    typedef ListNode<T> Node;
    typedef __list_iterator<T, Ref, Ptr> self;
    Node* _node;
    __list_iterator(Node* x)
      :_node(x)
    {}
    // ++it
    self& operator++()
    {
      _node = _node->_next;
      return *this;
    }
    // it++
    self operator++(int)
    {
      self tmp(*this);
      _node = _node->_next;
      return tmp;
    }
    self& operator--()
    {
      _node = _node->_prev;
      return *this;
    }
    self operator--(int)
    {
      self tmp(*this);
      _node = _node->_prev;
      return tmp;
    }
    Ref operator*()
    {
      return _node->_data;
    }
    Ptr operator->()
    {
      return &_node->_data;
    }
    bool operator!=(const self& s)
    {
      return _node != s._node;
    }
    bool operator==(const self& s)
    {
      return _node == s._node;
    }
  };
  template<class T>
  class list
  {
    typedef ListNode<T> Node;
  public:
    typedef __list_iterator<T, T&, T*> iterator;
    list()
    {
      _head = new Node(T());
      _head->_prev = _head;
      _head->_next = _head;
    }
    iterator begin()
    {
      return _head->_next;
    }
    iterator end()
    {
      return _head;
    }
    void swap(list<T>& tmp)
    {
      std::swap(_head, tmp._head);
    }
    void push_back(const T& x)
    {
      insert(end(), x);
    }
    iterator insert(iterator pos, const T& x)
    {
      Node* cur = pos._node;
      Node* newNode = new Node(x);
            
      // prev newNode cur
      Node* prev = cur->_prev;
      prev->_next = newNode;
      newNode->_prev = prev;
      newNode->_next = cur;
      cur->_prev = newNode;
      
      return newNode;
    }
  private:
    Node* _head = nullptr;
  };
}

push_back() 加上右值版本:

template<class T>
struct ListNode
{
    ListNode(T&& x)
      :_next(nullptr)
        ,_prev(nullptr)
        ,_data(forward<T>(x))// 完美转发
    {}
};
template<class T>
class list
{
  void push_back(T&& x)
    {
        insert(end(), forward<T>(x));// 关键处,完美转发
    }
    iterator insert(iterator pos, T&& x)
    {
        Node* cur = pos._node;
        Node* newNode = new Node(forward<T>(x));// 完美转发
        // prev newNode cur
        Node* prev = cur->_prev;
        prev->_next = newNode;
        newNode->_prev = prev;
        newNode->_next = cur;
        cur->_prev = newNode;
        return newNode;
    }
};

4. 针对 移动构造 和 移动赋值重载

  • 如果自己没有实现 移动构造移动赋值重载,且没有实现析构函数、拷贝构造、赋值重载的任意一个,编译器会自动生成默认的移动构造或移动赋值重载。

内置类型,完成浅拷贝;自定义类型,调用实现的移动构造(没实现移动构造,则调用拷贝构造)。

  • 如果提供了移动构造或移动赋值,则编译器不会自动生成拷贝构造和拷贝赋值
相关文章
C++11 右值引用和移动语义(三)
C++11 右值引用和移动语义
48 0
|
编译器 C++ 容器
C++11 右值引用和移动语义(二)
C++11 右值引用和移动语义
43 0
|
6月前
|
存储 安全 程序员
C++11:右值引用
C++11:右值引用
45 0
|
6月前
|
编译器 C++
c++左值、右值引用和移动语义
c++左值、右值引用和移动语义
56 0
|
6月前
|
安全 编译器 C++
C++11 右值引用和移动语义(一)
C++11 右值引用和移动语义
31 1
|
存储 编译器 C语言
C++11:右值引用和移动语义
C++11:右值引用和移动语义
61 1
|
6月前
|
编译器 C++
深入理解 C++ 右值引用和移动语义:全面解析
C++11引入了右值引用,它也是C++11最重要的新特性之一。原因在于它解决了C++的一大历史遗留问题,即消除了很多场景下的不必要的额外开销。即使你的代码中并不直接使用右值引用,也可以通过标准库,间接地从这一特性中收益。为了更好地理解该特性带来的优化,以及帮助我们实现更高效的程序,我们有必要了解一下有关右值引用的意义。
94 0
|
编译器 C++
C++10中的移动语义
对象的移动语义(Move Semantics)需要实现移动构造函数(move constructor)和移动赋值运算符(move assignment operator)。如果源对象是在复制或者赋值结束以后被销毁的临时对象,编译器会使用两种方法。移动构造函数和移动赋值运算符将成员变量从源对象复制/移动到新对象,然后将源对象的变量设置为空值。这样做实际上将内存的所有权从一个对象转移到另一个对象。这两种方法基本上只对成员变量进行浅拷贝(shallow copy),然后转换已分配内存的权限,从而防止悬挂指针和内存泄露。
75 0
C++10中的移动语义
|
编译器 C语言 C++
【C++】右值引用
【C++】右值引用
73 0