【C++】C++11右值引用|新增默认成员函数|可变参数模版|lambda表达式(上)

简介: 【C++】C++11右值引用|新增默认成员函数|可变参数模版|lambda表达式(上)

1. 右值引用和移动语义


1.1 左值引用和右值引用

在C++11之前,我们只有引用的概念,没有接触到所谓的左值引用或者是右值引用这种概念,从C++11开始,增加了右值引用的概念,那么现在我们将对引用进行一个概念上的区分。在此之前我们所说的引用都是左值引用,对于左值引用相关的内容,可以去看一看博主之前写的文章C++引用


不管是左值引用还是右值引用,本质上都是给对象取别名

那么,怎么区别左值引用和右值引用呢?

左值是一个表示数据的表达式(一个变量名或者是解引用的指针),我们可以获取到他的地址+可以对它进行赋值,左值可以在等号的左边,右值不能在等号的左边,const修饰的变量不能被赋值,但是能够取地址。

右值也是一个表达式,如:字面量,表达式的返回值,函数的返回值,右值能够出现在赋值符号的右边,不能出现在赋值符号的左边,右值不能取地址。

左值引用就是对左值进行取别名操作,右值引用就是对右值取别名

void Test1()
{
    //左值
    int a = 1;
    double x = 1.1, y = 2.2;
    int* pb = new int(10);
    const int c = 2;
    //左值引用
    int& ra = a;
    int*& rpb = pb;
    const int& rc = c;
    int& pvalue = *pb;
    //右值
    10;
    x + y;
    min(x, y);
    //右值引用
    int&& rr1 = 10;
    int&& rr2 = x + y;
    int&& rr3 = min(x, y);
}

一个有趣的现象:

我们知道,右值是不能被赋值的,但是看下面这段代码

void Test2()
{
    int x = 1, y = 2;
    int&& rr1 = x + y;
    cout << "x + y:" << x + y << endl;
    cout << "rr1:" << rr1 << endl;
    rr1 = 10;
    cout << "rr1" << rr1 << endl;
}

右值x+y在被右值引用之后就可以被赋值了,即变成了左值

这是因为在给右值取别名之后,会被存储在一个特定的位置,然后就能取到该位置的地址,因此也就能更改次地址存放的值。如果不想让它能被能改就可以使用const修饰右值引用。当然实际应用中不会使用到这个特性,所以这个特性也就不重要


1.2 左值引用和右值引用的比较

左值引用的总结:

  • 左值引用只能引用左值,不能引用右值
  • const左值引用既能引用左值,也能引用右值(这个跟我们之前所说的临时变量具有常性可以对照,那个临时变量就是右值)
void Test3()
{
    //左值引用只能引用左值,不能引用右值
    int a = 10;
    int& ra1 = a;
    //int& ra2 = 10;//右值引用不能引用左值,因此这行代码报错
    //const左值引用既能引用左值,也能引用右值
    const int& ra3 = 10;//引用右值
    const int& ra4 = a;//引用左值
}

右值引用的总结:

  • 右值引用只能引用右值,不能引用左值
  • move函数可以将左值变为右值,因此右值引用可以引用move之后的左值
void Test4()
{
    //右值引用只能引用右值,不能引用左值
    int&& r1 = 10;
    int a = 10;
    //int&& r2 = a;//右值引用引用左值报错
    //Xcode报错内容:Rvalue reference to type 'int' cannot bind to lvalue of type 'int'(无法将左值绑定到右值引用)
    //右值引用能引用move之后的左值
    int&& r3 = std::move(a);
}

1.3右值引用的使用场景和意义

既然在C++11之前已经有了左值引用,为什么还要加上右值引用这种概念呢?不是“画蛇添足”吗?

首先我们来总结一下左值引用的好处

  • 做函数参数:能够减少拷贝,提高效率,可以作为输出型参数
  • 做返回值:能够减少拷贝,提高效率


虽然左值引用有以上优点,但是,如果遇到下面的状况:

//状况一
template<class T>
T func(const T& val)
{
    T ret;
    //...
    return ret;
}
//状况二
void Test5()
{
    zht::string str;
    str = zht::to_string(-1234);
}


此时ret和str出了作用域之后就会销毁,如果T是类似string的对象,就需要进行深拷贝,所以就会造成效率降低。

这里我们采用自己实现的string,能够更清晰的看到调用情况:在这个string中增加了一些输出信息

namespace  zht
{
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)
    {
        std::swap(_str, s._str);
        std::swap(_size, s._size);
        std::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=(string s) -- 深拷贝 " << endl;
        if (this != &s)
        {
            delete[] _str;
            _str = new char[s._capacity + 1];
            strcpy(_str, s._str);
            _size = s._size;
            _capacity = s._capacity;
        }
        return *this;
    }
    ~string()
    {
        delete[] _str;
        _str = nullptr;
    }
    char& operator[](size_t pos)
    {
        assert(pos < _size);
        return _str[pos];
    }
    void reserve(size_t n)
    {
        if (n > _capacity)
        {
            char* tmp = new char[n + 1];
            strcpy(tmp, _str);
            delete[] _str;
            _str = tmp;
            _capacity = n;
        }
    }
    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';
    }
    string& operator+=(char ch)
    {
        push_back(ch);
        return *this;
    }
    const char* c_str() const
    {
        return _str;
    }
private:
    char* _str;
    size_t _size;
    size_t _capacity; // 不包含最后做标识的\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;
}
}


运行上述情况二的代码可以看到:在过程中进行了深拷贝,这里深拷贝的代价就非常的大,也就是左值引用的短板所在,因此提出了右值引用的概念。

8151b99c61693e9531881441813fa8e9.png

使用右值引用和移动语义解决上述问题:

在此之前,我们明确一个概念:在C++11中,对右值引用进行了一个分类,将其分为纯右值和将亡值两种,其中纯右值指的是内置类型表达式的值,将亡值是指自定义类型的表达式的值,所谓的将亡值也就是指生命周期将要结束的值,一般来说,匿名对象、临时对象、move后的自定义类型对象都是将亡值。

让我们思考一下,在上述情景过程中,进行深拷贝之后,原来的局部变量str是会被析构掉的,相当于我们先构造一个一摸一样的变量,然后再析构掉这个局部变量,那么我们不如直接将这个变量的资源交给另一个变量管理,这样就能够提高效率。

于是就有了移动构造这个接口:移动构造也是一个构造函数,它的参数是类型的右值引用,实际上就是把传入右值的资源转移过来,避免了深拷贝,所以称为移动构造,就是移动别人的资源来进行构造。

接下来我们来实现一下上面string的移动构造和移动赋值

// 移动构造
string(string&& s)
    :_str(nullptr)
    , _size(0)
    , _capacity(0)
{
  cout << "string(string&& s) -- 移动构造" << endl;
  swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
    cout << "string& operator=(string&& s) -- 移动赋值" << endl;
    swap(s);
    return *this;
}

在我们自己实现的string中加入了这两个函数之后,再次运行刚刚的程序,就会发现函数调用改变了:深拷贝变成了移动拷贝

f2cb5cfa84738a895607a5ad0755bc86.png

下面,我们来分析一下为什么会变成这样:

加入移动语义之前:

63f9527664797fe41f27bbba80bf5678.png

加入移动语义之后:

dc1e53fe64ff15680aae446ef52ba430.png

注:有种说法是右值引用延长了变量的生命周期,事实上这种说法是不准确的,他只是将一个变量的资源转移给了另一个变量,此变量本身的生命周期是没有变化的。如果一定要这样说的话,那可以理解成延长了这个资源的生命周期(但是资源没有生命周期这一说)。


在C++11之后,STL容器中都增加了移动构造和移动复制的接口

ddb3640a32a0be4591c03e20b390421e.png

dbe6513b119ac191f08398cfdfc088bf.png


1.4 左值引用和右值引用的深入使用场景分析

根据上文,我们知道右值只能引用右值,但是右值一定不能引用左值吗?

在有些场景下,我们可能真的需要右值去引用左值,从而实现移动语义。

当需要右值引用一个左值的时候,可以通过move函数将左值转变为右值。在C++11中,std::move()函数在<utility>头文件中,这个函数只有一个唯一的作用就是将左值强制转换成右值

f654fd42d4d8cc81104d1978b63697b5.png

可以看到使用move将s1从左值变成右值之后,再次插入编译器就将s1识别成将亡值,匹配到移动构造然后未查到lt中。

根据上述的例子我们知道库里面的list是支持移动构造的,我们之前也模拟实现过list,那么现在能否对之前的list进行一个改造,让其也能支持移动语义呢?


首先在这里附上之前实现的list的源码

namespace zht
{
  template<class T>
  struct __list_node
  {
    __list_node* _prev;
    __list_node* _next;
    T _data;
    __list_node(const T& data = T())
      :_data(data)
      , _prev(nullptr)
      , _next(nullptr)
    {}
  };
  template<class T, class Ref, class Ptr>
  struct __list_iterator
  {
    typedef __list_node<T> node;
    typedef __list_iterator<T, Ref, Ptr> Self;
    node* _pnode;
    __list_iterator(node* p)
      :_pnode(p)
    {}
    Ptr operator->()
    {
      return &operator*();
    }
    Ref operator*()
    {
      return _pnode->_data;
    }
    Self& operator++()
    {
      _pnode = _pnode->_next;
      return *this;
    }
    Self operator++(int)
    {
      Self tmp(*this);
      _pnode = _pnode->_next;
      return tmp;
    }
    Self& operator--()
    {
      _pnode = _pnode->_prev;
      return *this;
    }
    Self operator--(int)
    {
      Self tmp(*this);
      _pnode = _pnode->_prev;
      return tmp;
    }
    bool operator!=(const Self& it)
    {
      return _pnode != it._pnode;
    }
    bool operator==(const Self& it)
    {
      return _pnode == it._pnode;
    }
  };
  template<class T>
  class list
  {
    typedef __list_node<T> node;
  public:
    typedef __list_iterator<T, T&, T*> iterator;
    typedef __list_iterator<T, const T&, const T*> const_iterator;
    iterator begin()
    {
      return _head->_next;
    }
    iterator end()
    {
      return _head;
    }
    const_iterator begin() const
    {
      return _head->_next;
    }
    const_iterator end() const
    {
      return _head;
    }
    void empty_initialize()
    {
      _head = new node(T());
      _head->_next = _head;
      _head->_prev = _head;
      _size = 0;
    }
    list()
    {
      empty_initialize();
    }
    list(size_t n, const T& val = T())
    {
      empty_initialize();
      for (size_t i = 0; i < n; ++i)
      {
        push_back(val);
      }
    }
    template<class InputIterator>
    list(InputIterator first, InputIterator last)
    {
      empty_initialize();
      while (first != last)
      {
        push_back(*first);
        ++first;
      }
    }
    list(const list<T>& lt)//经典写法
    {
        empty_initialize();
        for (const auto& e : lt)
        {
            push_back(e);
        }
    }
    void swap(list<T>& lt)
    {
      std::swap(_head, lt._head);
    }
    list<T>& operator=(list<T>& lt)
    {
      if (this != *lt)
      {
        clear();
        for (auto& e : lt)
        {
          push_back(e);
        }
      }
    }
    bool empty()
    {
      return _head->_next == _head;
    }
    void clear()
    {
      while (!empty())
      {
        erase(--end());
      }
    }
    ~list()
    {
      clear();
      delete _head;
      _head = nullptr;
    }
    void push_back(const T& val = T())
    {
      insert(end(), val);
    }
    void push_front(const T& val = T())
    {
      insert(begin(), val);
    }
    iterator insert(iterator pos, const T& val = T())
    {
      node* newnode = new node(val);
      node* prev = pos._pnode->_prev;
      node* cur = pos._pnode;
      prev->_next = newnode;
      newnode->_prev = prev;
      newnode->_next = cur;
      cur->_prev = newnode;
      ++_size;
      return iterator(newnode);
    }
    size_t size()
    {
      return _size;
    }
    iterator erase(iterator pos)
    {
      assert(pos != end());
      node* prev = pos._pnode->_prev;
      node* next = pos._pnode->_next;
      prev->_next = next;
      next->_prev = prev;
      delete pos._pnode;
      --_size;
      return iterator(next);
    }
    void pop_back()
    {
      erase(--end());
    }
    void pop_front()
    {
      erase(begin());
    }
    void resize(size_t n, const T& val = T())
    {
      while (n < size())
      {
        pop_back();
      }
      while (n > size())
      {
        push_back(val);
      }
    }
  private:
    node* _head;
    size_t _size;
  };
}


使用我们自己实现的list执行1.4中的Test6,将会得到如下结果:

899625c47cbde5d0b778ac11f92c99fd.png

为了让我们的list也能像库里面的list一样,我们首先考虑到的就是实现push_back的右值引用版本,由于在push_back中调用了insert,所以insert也需要增加右值引用的版本,同样的在node的构造中也需要增加右值引用版本的构造函数,所以增加的函数如下:

//template<class T> struct __list_node
__list_node(T&& data)
    :_data(data)
    , _prev(nullptr)
    , _next(nullptr)
{}
//template<class T> class list
void push_back(T&& val)
{
    insert(end(), val);
}
iterator insert(iterator pos, T&& val)
{
    node* newnode = new node(val);
    node* prev = pos._pnode->_prev;
    node* cur = pos._pnode;
    prev->_next = newnode;
    newnode->_prev = prev;
    newnode->_next = cur;
    cur->_prev = newnode;
    ++_size;
    return iterator(newnode);
}

那么现在再来尝试执行以下,发现没有变化,这是为什么呢?


(这里大家可以自己去实践一下)通过调试可以发现确实调用了右值引用版本的push_back,但是继续往下面调试就发现调用的是左值版本的insert,这是什么原因呢??


因为右值引用之后变量本身是左值,所以这里val的属性就是一个左值,所以当然会匹配到左值版本的insert,所以这里在传参的时候需要将这个val的属性变成右值,这里可以使用move来改一下传参之后的属性,然后再编译运行,发现还是和原来的结果一样,这是因为函数套函数,每一层都需要将参数属性改为右值,非常的麻烦,如果我们将所有的参数都用move修改一下之后:

__list_node(T&& data)
    :_data(move(data))
    , _prev(nullptr)
    , _next(nullptr)
{}    
void push_back(T&& val)
{
    insert(end(), move(val));
}
iterator insert(iterator pos, T&& val)
{
    node* newnode = new node(move(val));
    node* prev = pos._pnode->_prev;
    node* cur = pos._pnode;
    prev->_next = newnode;
    newnode->_prev = prev;
    newnode->_next = cur;
    cur->_prev = newnode;
    ++_size;
    return iterator(newnode);
}

再次运行之前的代码,可以看到已经实现了库里面的效果,这里多了一个string的构造和移动构造是因为初始化lt的时候调用的,由于我们string和list的实现和库里面还是有些许不同点的。

fbc76b616f80d83cf8cda70a704b507b.png

至此,我们就完成了自己的list中对移动语义的支持了。

相关文章
|
2天前
|
算法 安全 编译器
【C++】从零开始认识泛型编程 — 模版
泛型编程是C++中十分关键的一环,泛型编程是C++编程中的一项强大功能,它通过模板提供了类型无关的代码,使得C++程序可以更加灵活和高效,极大的简便了我们编写代码的工作量。
13 3
|
2天前
|
存储 算法 C++
C++11:lambda表达式 & 包装器
C++11:lambda表达式 & 包装器
7 0
|
3天前
|
存储 安全 C++
深入理解C++中的指针与引用
深入理解C++中的指针与引用
6 0
|
4天前
|
存储 算法 对象存储
【C++入门到精通】function包装器 | bind() 函数 C++11 [ C++入门 ]
【C++入门到精通】function包装器 | bind() 函数 C++11 [ C++入门 ]
14 1
|
4天前
|
算法 C++
【C++入门到精通】condition_variable(条件变量)C++11 [ C++入门 ]
【C++入门到精通】condition_variable(条件变量)C++11 [ C++入门 ]
9 0
|
4天前
|
安全 算法 程序员
【C++入门到精通】Lock_guard与Unique_lock C++11 [ C++入门 ]
【C++入门到精通】Lock_guard与Unique_lock C++11 [ C++入门 ]
8 0
|
4天前
|
算法 安全 C++
【C++入门到精通】互斥锁 (Mutex) C++11 [ C++入门 ]
【C++入门到精通】互斥锁 (Mutex) C++11 [ C++入门 ]
8 0
|
4天前
|
设计模式 安全 算法
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
15 0
|
5天前
|
C语言 C++
【C++】string类(常用接口)
【C++】string类(常用接口)
14 1
|
2天前
|
编译器 C++
【C++】继续学习 string类 吧
首先不得不说的是由于历史原因,string的接口多达130多个,简直冗杂… 所以学习过程中,我们只需要选取常用的,好用的来进行使用即可(有种垃圾堆里翻美食的感觉)
7 1