移动语义和引用折叠、完美转发

移动构造、移动赋值

C++11新增了移动语义新特性,移动语义允许在不复制数据的情况下转移资源的所有权。在这之前,对象通过拷贝构造函数拷贝赋值运算符进行传递,发生大量的数据复制,导致性能下降。

以常用的string对象为例,

#include <cstring>
#include <iostream>
class string {
public:
  string(const char *p = nullptr) {
    std::cout << "default ctor" << std::endl;
    if (p != nullptr) {
      _data = new char[strlen(p) + 1];
      strcpy(_data, p);
    } else {
      _data = new char[1];
      *_data = '\0';
    }
  }
  ~string() {
    std::cout << "destructor" << std::endl;
    delete[] _data;
    _data = nullptr;
  }
  string(const string &str) {
    std::cout << "copy ctor" << std::endl;
    _data = new char[strlen(str._data) + 1];
    strcpy(_data, str._data);
  }
  string &operator=(const string &str) {
    std::cout << "copy assignment" << std::endl;
    if (this == &str) {
      return *this;
    }
    delete[] _data;
    _data = new char[strlen(str._data) + 1];
    strcpy(_data, str._data);
    return *this;
  }
  string(string &&str) {
    std::cout << "move ctor" << std::endl;
    _data = str._data;
    str._data = nullptr;
  }
  string &operator=(string &&str) {
    std::cout << "move assignment" << std::endl;
    if (this == &str)
      return *this;
    delete[] _data;
    _data = str._data;
    str._data = new char[1];
    str._data[0] = '\0';
    return *this;
  }
  const char *c_str() const { return _data; }

private:
  char *_data;
};
string foo(const string &val) {
  const char *str = val.c_str();
  string tmp(str);
  return tmp;
}
int main() {
  string str1("hello");
  string str2("world");
  str2 = foo(str1);
  std::cout << str2.c_str() << std::endl;
  return 0;
}

如果没有移动语义,上面这段代码中会发生两次拷贝,
第一次是foo函数的返回,会发生一次拷贝构造main函数栈帧上的临时对象(开辟内存,拷贝数据),然后析构tmp(释放内存)。
第二次是str2的拷贝赋值,将main函数栈帧上的临时对象拷贝赋值给str2,又是开辟内存,拷贝数据,然后析构临时对象。

因此没有移动语义,对象的传递将会发生大量的拷贝,尤其是各种临时对象的返回以及临时对象的赋值运算重载。

在增加移动语义后,由tmp构造main函数栈帧临时对象时,不用开辟新的内存,而是直接把tmp中的char*移动到临时对象中,这种资源的所有权转移,避免了内存的开辟释放,以及数据拷贝。同理,main函数栈帧临时对象赋值给str2时,也是直接转移char*即可。

这便是移动语义的好处:减少不必要的内存开辟和释放以及数据拷贝。

std::move()的使用

一个很重要的点:

  • 右值引用变量本身是一个左值,因此必须用左值引用来引用一个右值引用变量

那这会导致什么问题呢?
试想这样一个场景,函数接收一个右值引用作为参数,而这个右值引用变量在函数内部又需要作为另一个函数的参数。假设在内部的这个函数里,想要执行资源的所有权的转移,那么,必须在这个内部函数中,将这个右值引用变量转化为一个右值

这可能理解起来有点困难,我们来举一个例子。

#include <iostream>

class Foo {
public:
  Foo() {}
  Foo(const Foo &) { std::cout << "copy constructor" << std::endl; }
  Foo(Foo &&) { std::cout << "move constructor" << std::endl; }
};

void bar(Foo &&val2) { Foo f(std::move(val2)); }
void foo(Foo &&val1) { bar(std::move(val1)); }

int main() { foo(Foo()); }

main函数中的临时对象传给foo函数,在foo函数内部,必须调用std::move(val1),才能调用bar(Foo &&val2)函数,否则提示函数不匹配。
同样的,在bar函数内部,也必须执行std::move(val2),才能使用移动构造函数,构造f,否则使用的是拷贝构造函数。

所以std::move()可以用于将一个左值变为右值,尤其是对于一个右值引用参数是一个左值的事实。

引用折叠、完美转发

首先是理解万能引用这个概念,万能引用是指在模板函数或模板类中,使用T &&作为参数的情况,它可以绑定到左值或右值,取决于传递给模板参数的具体类型。

万能引用离不开模板,必须使用模板类型参数T以及&&构成T &&才算是万能引用。

一个比较常见的例子是,vector容器的push_back函数,既可以接收一个左值,也可以接收一个右值。然而,在函数内部的逻辑是完全一样的,所以为了将两个函数void push_back(const T&)void push_back(T &&)合并成一个函数,就出现了引用折叠和完美转发。

引用折叠的规则

  • 所有右值引用折叠到右值引用上仍然是一个右值引用。(T&& && 变成 T&&)
  • 所有的其他引用类型之间的折叠都将变成左值引用。 (T& & 变成 T&; T& && 变成 T&; T&& & 变成 T&)

那么如何理解呢?

#include <iostream>

template <typename T>
void foo(T &t) { 
  std::cout << "Lvalue ref" << std::endl; 
}

template <typename T>
void foo(T &&t) {
  std::cout << "Rvalue ref" << std::endl;
}

template <typename T>
void bar(T &&v) { // 万能引用,既可以接收左值引用,也可以接收右值引用
  foo(v); // 无论v是左值引用还是右值引用,v都是一个左值
  foo(std::move(v)); // 通过std::move将左值v变成一个右值
  foo(std::forward<T>(v)); // 将v强转成类型T,而T是模板参数类型自动推导得到的
}

int main(int argc, char *argv[]) {
  int x = 1;
  bar(x);
  std::cout << "=======" << std::endl;
  bar(std::move(x));
}

这段代码的执行结果如下:

Lvalue ref
Rvalue ref
Lvalue ref
=======
Lvalue ref
Rvalue ref
Rvalue ref

可以发现

  • foo(v);调用的都是左值的
  • foo(std::move(v));调用的都是右值的
  • foo(std::forward<T>(v));则是根据v的引用类型T调用对应的版本

关注对bar(T &&v)的调用:
bar(x) ==> T &&v 等价于 int& ==> T是int&
bar(std::move(x)); ==> T &&v 等价于 int&& ==> T是int&&
然后std::forward<T>(v)中利用static_cast将v强转成对应类型,从而实现完美转发。

所以完美转发=万能引用 + 引用折叠 + std::forward(val)

我们可以看一下std::forward的源码

// 转发一个左值
template<typename _Tp>
  _GLIBCXX_NODISCARD
  constexpr _Tp&&
  forward(typename std::remove_reference<_Tp>::type& __t) noexcept
  { return static_cast<_Tp&&>(__t); }

// 转发一个右值
template<typename _Tp>
  _GLIBCXX_NODISCARD
  constexpr _Tp&&
  forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
  {
    static_assert(!std::is_lvalue_reference<_Tp>::value,
  "std::forward must not be used to convert an rvalue to an lvalue");
    return static_cast<_Tp&&>(__t);
  }

然后,在来看看vector容器中,如何利用完美转发,实现push_back函数。它的大致过程包括:push_back中使用万能引用接收左值引用或者右值引用,函数内部则
使用std::forward将对象完美转发给容器空间配置器Allocator的construct函数中,construct函数再使用定位new(placement new)在对应的内存地址上构造对象,也是使用std::forward将对象传递给对应的拷贝构造或者移动构造函数。

#include <algorithm>
#include <cstring>
#include <iostream>
#include <iterator>
// 容器空间配置器
template <typename T> 
class Allocator {
public:
  T *allocate(size_t size) { return (T *)malloc(sizeof(T) * size); }
  void deallocate(void *p) { free(p); }

  // 引用折叠,类型完美转发
  template <typename Ty> 
  void construct(T *p, Ty &&val) {
    new (p) T(std::forward<Ty>(val));
  } // placement new,也叫定位new
  void destry(T *p) { p->~T(); }
};

template <typename T, typename Alloc = Allocator<T>> 
class vector {
public:
  vector(int capacity = 10) {
    // _first = new T[capacity];
    _first = _alloc.allocate(capacity);
    _last = _first;
    _end = _first + capacity;
  }
  ~vector() {
    // delete[] _first;
    for (T *p = _first; p != _last; ++p) {
      _alloc.destry(p);
    }
    _alloc.deallocate(_first);
    _first = _last = _end = nullptr;
  }

  vector(const vector<T> &val) {
    auto capacity = val._end - val._first;
    auto size = val._last - val._first;
    // _first = new T[capacity];
    _first = _alloc.allocate(capacity);
    for (int i = 0; i < size; ++i) {
      // _first[i] = val._first[i];
      _alloc.construct(_first + i, val._first[i]);
    }
    _last = _first + size;
    _end = _first + capacity;
  }

  vector<T> &operator=(const vector<T> &val) {
    if (this == &val) {
      return *this;
    }
    // delete[] _first;
    for (T *p = _first; p != _last; ++p) {
      _alloc.destry(p);
    }
    _alloc.deallocate(_first);

    auto capacity = val._end - val._first;
    auto size = val._last - val._first;
    // _first = new T[capacity];
    _first = _alloc.allocate(capacity);
    for (int i = 0; i < size; ++i) {
      // _first[i] = val._first[i];
      _alloc.construct(_first + i, val._first[i]);
    }
    _last = _first + size;
    _end = _first + capacity;

    return *this;
  }

  template <typename Ty> 
  void push_back(Ty &&val) { // 引用折叠
    if (full())
      expand();
    // *_last++ = val;
    _alloc.construct(_last, std::forward<Ty>(val));
    ++_last;
  }

  void pop_back() {
    if (empty())
      return;
    // --_last;
    --_last;
    _alloc.destry(_last);
  }

  T back() const { return *(_last - 1); }
  bool full() const { return _last == _end; }

  bool empty() const { return _first == _last; }
  int size() const { return _last - _first; }

  T &operator[](int index) { return _first[index]; }

  class iterator {
  public:
    iterator(T *ptr = nullptr) : _p(ptr) {}

    bool operator!=(const iterator &val) const { return _p != val._p; }

    void operator++() { ++_p; }

    T &operator*() { return *_p; }

  private:
    T *_p;
  };

  iterator begin() { return {_first}; }
  iterator end() { return {_last}; }

private:
  void expand() {
    int capacity = _end - _first;
    int size = _last - _first;
    // T *ptmp = new T[capacity * 2];
    T *ptmp = _alloc.allocate(2 * capacity);
    for (int i = 0; i < size; ++i) {
      // ptmp[i] = _first[i];
      _alloc.construct(ptmp + i, _first[i]);
    }
    // delete[] _first;
    for (T *p = _first; p != _last; ++p) {
      _alloc.destry(p);
    }
    _alloc.deallocate(_first);
    _first = ptmp;
    _last = _first + size;
    _end = _first + capacity * 2;
  }

private:
  T *_first;
  T *_last;
  T *_end;
  Alloc _alloc;
};

class string {
public:
  friend string operator+(const string &l, const string &r);
  string(const char *p = nullptr) {
    std::cout << "default ctor" << std::endl;
    if (p != nullptr) {
      _data = new char[strlen(p) + 1];
      strcpy(_data, p);
    } else {
      _data = new char[1];
      *_data = '\0';
    }
  }

  ~string() {
    delete[] _data;
    _data = nullptr;
  }

  string(const string &str) {
    std::cout << "copy ctor" << std::endl;
    _data = new char[strlen(str._data) + 1];
    strcpy(_data, str._data);
  }

  string &operator=(const string &str) {
    if (this == &str) {
      return *this;
    }
    delete[] _data;
    _data = new char[strlen(str._data) + 1];
    strcpy(_data, str._data);
    return *this;
  }

  string(string &&str) {
    std::cout << "move ctor" << std::endl;
    _data = str._data;
    str._data = nullptr;
  }

  string &operator=(string &&str) {
    if (this == &str)
      return *this;
    delete[] _data;
    _data = str._data;
    str._data = new char[1];
    str._data[0] = '\0';
    return *this;
  }

  bool operator>(const string &val) const {
    return strcmp(_data, val._data) > 0;
  }

  bool operator<(const string &val) const {
    return strcmp(_data, val._data) < 0;
  }

  bool operator==(const string &val) const {
    return strcmp(_data, val._data) == 0;
  }

  // 仅普通对象能调用,可以读,也可以写
  // 读 char ch = str[1];
  // 写 str[1] = 'c';
  char &operator[](int idx) { return _data[idx]; }

  // 普通对象和常对象都能调用,只可以读
  // 读 char ch = str[1];
  const char &operator[](int idx) const { return _data[idx]; }
  //
  const char *c_str() const { return _data; }

  int length() const { return strlen(_data); }

  // 迭代器可以透明地访问容器内部的元素
  class iterator {
  public:
    iterator(char *p = nullptr) : _p(p) {}
    bool operator!=(const iterator &it) { return _p != it._p; }
    void operator++() { ++_p; }
    char &operator*() { return *_p; }

  private:
    char *_p;
  };

  iterator begin() { return {_data}; }
  iterator end() { return {_data + length()}; }

private:
  char *_data;
};

std::ostream &operator<<(std::ostream &out, const string &val) {
  out << val.c_str();
  return out;
}

string operator+(const string &l, const string &r) {
  string tmp; // default
  delete[] tmp._data;
  tmp._data = new char[strlen(l._data) + strlen(r._data) + 1];
  strcpy(tmp._data, l._data);
  strcpy(tmp._data + strlen(l._data), r._data);
  return tmp;
}

int main() {
  vector<string> v;
  string str1 = "hello";
  v.push_back(str1);
  v.push_back(string("world"));
}

这段代码的运行结果如下:
default ctor
copy ctor
default ctor
move ctor

重点关注以下函数

template <typename Ty> 
void construct(T *p, Ty &&val) {  // 引用折叠
  new (p) T(std::forward<Ty>(val));  // 类型完美转发
} 

template <typename Ty> 
void push_back(Ty &&val) { // 引用折叠
  if (full())
    expand();
  // *_last++ = val;
  _alloc.construct(_last, std::forward<Ty>(val)); // 类型完美转发
  ++_last;
}
posted @ 2024-04-23 22:31  EricLing0529  阅读(11)  评论(0编辑  收藏  举报