Стандарт С++11: rvalue и перемещение
дата публикации: 2016-10-30

Стандарт С++11 существенно дополнил функционал языка и его в какой-то степени можно считать революционным. Задача данной статьи - рассмотрение одной из новелл стандарта, а именно - функционала перемещения, но сначала разберем предпосылки появления новеллы.

Обращение к ячейке памяти, не важно оперативной или другого типа, обычно требует определенных затрат времени. Впрочем это становится очевидным, когда мы что-то скачиваем на флешку (копирование во flash память), либо даже загружаем программу (копирование в оперативную память). Понятно, что без этих затрат не обойтись, если мы переносим информацию на другой носитель, но вот зачастую в коде можно встретить большое количество копирований из одних ячеек памяти в другие. Приведу типовые примеры:

  • существует два обработчика, которые работают с одними и теми же данными и, чтобы не произошло конфликта, эти данные, на момент выполнения задания, существуют в памяти в двух экземплярах.
  • на период осуществления каких-либо операций с данными производится их дополнительное копирование, например прежде чем осуществлять сортировку, сначала дублируется исходная структура.

Перечень не претендует на абсолют и не является законченным. Теперь несколько слов о методах и операторах, которые так или иначе связаны с процессами создания и удаления экземпляров. В соответствии со стандартом С++11 таких методов и операторов шесть:

  • 1) конструктор создания;
  • 2) конструктор копирования;
  • 3) оператор копирования;
  • 4) конструктор перемещения;
  • 5) оператор перемещения;
  • 6) деструктор.

Все эти методы представлены далее в коде. В чем же принципиальное отличие копирования от перемещения? Конечно же перемещение можно заменить копированием в новое место и дальнейшим стиранием исходных данных, но это очень дорогая операция. Понятно, что было бы правильным просто передать управление структурой данных новому указателю, при этом не забыв обнулить указатель исходный, чтобы потом не произвести двойное освобождение памяти. Именно так и поступают обычно, однако некоторые объекты живут в памяти временно и удаляются автоматически, например, возвращаемое функцией значение или результат вычисления выражения. В новом стандарте существует возможность передать управление от временного объекта по ссылке, при этом не уничтожая его после передачи. Такая ссылка называется rvalue, а операция - перемещение.

Ниже приведен код, в котором создается класс и все варианты методов по умолчанию.

class Array {
  int nArr, *pArr;
public:
  Array(); //конструктор создания
  Array(const int* pArr, const int nArr); //конструктор создания
  ~Array(); //деструктор
  Array(const Array& obj); //конструктор копирования
  Array& operator=(const Array& obj); //оператор копирования
  Array(Array&& obj); //конструктор перемещения
  Array& operator=(Array&& obj); //оператор перемещения
  void show();
};

Array::Array() {
  nArr = 0;
  pArr = nullptr;
  cout << "Constructor 1" << endl;
}

Array::Array(const int* pArr, const int nArr) {
  this->nArr = nArr;
  this->pArr = new int[nArr];
  copy(pArr, pArr + nArr, this->pArr);
  cout << "Constructor 2" << endl;
}

Array::~Array() {
  if(pArr != nullptr) {
      delete[] pArr;
  }
  cout << "Destructor" << endl;
}

Array::Array(const Array& obj) {
  nArr = obj.nArr;
  pArr = new int[nArr];
  copy(obj.pArr, obj.pArr + nArr, pArr);
  cout << "Copy constructor" << endl;
}

Array& Array::operator=(const Array& obj) {
  if(this != &obj) {
      if(pArr != nullptr) {
          delete[] pArr;
      }
      nArr = obj.nArr;
      pArr = new int[nArr];
      copy(obj.pArr, obj.pArr + nArr, pArr);
      cout << "Copy operator" << endl;
  }
  return *this;
}

Array::Array(Array&& obj) {
  pArr = obj.pArr;
  nArr = obj.nArr;
  obj.pArr = nullptr;
  obj.nArr = 0;
  cout << "Move constructor" << endl;
}

Array& Array::operator=(Array&& obj) {
  if(this != &obj) {
    if(pArr != nullptr) {
      delete[] pArr;
    }
    pArr = obj.pArr;
    nArr = obj.nArr;
    obj.pArr = nullptr;
    obj.nArr = 0;
    cout << "Move operator" << endl;
  }
  return *this;
}


void Array::show() {
  for(int i = 0; i < nArr; i++) {
    cout << "element " << i << " = " << *(pArr + i) << endl;
  }
  cout << "-----------" << endl;
}

int main() {
  int n = 3;
  int* testArr = new int[n];
  for(int i = 0; i < n; i++) {
    testArr[i] = 2 * i + 1;
  }

  Array arr1(testArr,n); // конструктор 2
  Array arr2(arr1); // конструктор копирования
  Array arr3, arr4; // конструкторы 1
  arr3 = arr2; // оператор копирования
  arr4 = move(arr3); // оператор перемещения
  Array arr5 = move(arr4); // конструктор перемещения
  cout << "-----------" << endl;
  cout << "arr1" << endl;
  arr1.show();
  cout << "arr2" << endl;
  arr2.show();
  cout << "arr3" << endl;
  arr3.show();
  cout << "arr4" << endl;
  arr4.show();
  cout << "arr5" << endl;
  arr5.show();
  return 0;
}

Результат работы кода:

Constructor 2
Copy constructor
Constructor 1
Constructor 1
Copy operator
Move operator
Move constructor
-----------
arr1
element 0 = 1
element 1 = 3
element 2 = 5
-----------
arr2
element 0 = 1
element 1 = 3
element 2 = 5
-----------
arr3
-----------
arr4
-----------
arr5
element 0 = 1
element 1 = 3
element 2 = 5
-----------
Destructor
Destructor
Destructor
Destructor
Destructor

Сделаем выводы и подведем итог. Мы определили два конструктора создания, один работает без параметров, во второй передаются параметры. Задача кода - последовательно передать (копированием или перемещением) массив от arr1 к arr5. Как можно видеть, после передачи через перемещение исходные данные более недоступны и массив пустой. Использование функции move() позволяет компилятору указать какой метод или оператор выбирать (копирование или перемещение) для выполнения операции.