Конструктор перемещения

Вместо создания копии объекта можно переместить ресурсы из одного объекта в другой. Выполнить перемещение позволяет конструктор перемещения, который вызывается при инициализации временным объектом (значением r-value), например, временным объектом, возвращаемым из функции. При возврате объекта из функции конструктор перемещения может не вызываться из-за оптимизаций вариантов RVO и NRVO компилятором (см. разд. 13.6). Чтобы вызвать конструктор перемещения явным образом можно воспользоваться функцией move(), которая выполняет приведение значения l-value к r-value:

// C obj3 = func2(2);
C obj3 = std::move(func2(2)); // Явный вызов конструктора перемещения

Конструктор перемещения имеет следующий формат:

<Название класса>(<Название класса> &&<Название объекта>);

Название конструктора перемещения совпадает с названием класса. В качестве параметра конструктор принимает ссылку на значение r-value. Тип возвращаемого значения не указывается. Продемонстрируем использование конструкторов копирования и перемещения на примере (листинг 13.10). Дополнительно выполним перегрузку оператора присваивания для операций копирования и перемещения.

Листинг 13.10. Конструкторы копирования и перемещения

#include <iostream>
#include <cstring>

class C {
   int *data_ = nullptr;
   unsigned size_ = 0;
public:
   C() {}
   C(unsigned size, int n=0) {
      if (size < 1) size_ = 1;
      else size_ = size;
      data_ = new int[size_];
      for (unsigned i = 0; i < size_; ++i) {
         data_[i] = n;
      }
   }
   C(const C &obj) {                  // Конструктор копирования
      std::cout << "copy" << std::endl;
      size_ = obj.size_;
      if (size_ == 0) { data_ = nullptr; return; }
      data_ = new int[size_];
      std::memcpy(data_, obj.data_, size_ * sizeof(int));
   }
   C &operator=(const C &obj) {       // Присваивание копированием
      std::cout << "operator=(const C &obj)" << std::endl;
      if (this == &obj) return *this; // Случай obj = obj
      if (data_) delete [] data_;
      size_ = obj.size_;
      if (size_ == 0) { data_ = nullptr; return *this; }
      data_ = new int[size_];
      std::memcpy(data_, obj.data_, size_ * sizeof(int));
      return *this;
   }
   C(C &&obj) {            // Конструктор перемещения
      std::cout << "move" << std::endl;
      size_ = obj.size_;
      data_ = obj.data_;
      obj.size_ = 0;
      obj.data_ = nullptr;
   }
   C &operator=(C &&obj) { // Присваивание перемещением
      std::cout << "operator=(C &&obj)" << std::endl;
      if (this == &obj) return *this; // Случай obj = obj
      if (data_) delete [] data_;
      size_ = obj.size_;
      data_ = obj.data_;
      obj.size_ = 0;
      obj.data_ = nullptr;
      return *this;
   }
   void dump() {
      std::cout << "[";
      if (data_) {
         for (unsigned i = 0; i < size_; ++i) {
            std::cout << data_[i] << " ";
         }
      }
      std::cout << "]" << std::endl;
   }
   unsigned getSize() { return size_; }
   int *getData() { return data_; }
   ~C() {
      if (data_) {
         delete [] data_;
         std::cout << "delete [] data_" << std::endl;
      }
      std::cout << "~C()" << std::endl;
   };
};

void func(C obj) {         // Вызывается конструктор копирования
   int *p = obj.getData();
   if (p && obj.getSize() > 0) p[1] = 66;
   obj.dump(); // [55 66 0 0 0 0 0 0 0 0 ]
}
C func2(int n) {
   C obj(10, n);
   return obj;
}

int main() {
   C obj(10);
   std::cout << obj.getSize() << std::endl; // 10
   obj.dump();           // [0 0 0 0 0 0 0 0 0 0 ]
   int *p = obj.getData();
   if (p) p[0] = 55;
   obj.dump();           // [55 0 0 0 0 0 0 0 0 0 ]

   func(obj);            // Вызывается конструктор копирования
   obj.dump();           // [55 0 0 0 0 0 0 0 0 0 ]

   C obj2;
   obj2.dump();          // []
   obj2 = obj;           // Вызывается метод operator=(const C &obj)
   int *p2 = obj2.getData();
   if (p2) p2[0] = 99;
   obj2.dump();          // [99 0 0 0 0 0 0 0 0 0 ]
   obj.dump();           // [55 0 0 0 0 0 0 0 0 0 ]

   // C obj3 = func2(2);
   C obj3 = std::move(func2(2)); // Явный вызов конструктора перемещения
   obj3.dump();                  // [2 2 2 2 2 2 2 2 2 2 ]

   C obj4;
   obj4 = func2(3);      // Вызывается метод operator=(C &&obj)
   obj4.dump();          // [3 3 3 3 3 3 3 3 3 3 ]
   return 0;
}

В этом примере внутри класса мы работаем с динамической памятью, поэтому необходимо явным образом реализовать как минимум конструктор копирования и оператор присваивания копированием, а также деструктор, внутри которого освобождается динамическая память. Если конструктор и метод не реализованы, то компилятор создает их реализацию автоматически, при этом выполняется поверхностное копирование объекта. Это означает, что будет скопирован адрес динамической памяти. В результате два объекта будут ссылаться на тот же самый участок памяти. Внутри деструктора мы вызываем оператор delete и освобождаем динамическую память. При вызове деструктора первого объекта все будет нормально, а вот вызов деструктора второго объекта приведет к попытке освободить уже не существующий участок памяти, что является ошибкой. Поэтому внутри конструктора копирования и метода присваивания копированием мы создаем полную копию объекта, выделяя динамическую память и копируя значения в нее с помощью функции memcpy(). Таким образом, объекты будут работать с разными участками памяти.

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

Итак, компилятор по умолчанию создает внутри класса следующие методы:

  • конструктор по умолчанию. Если вы создали конструктор с параметрами, то конструктор по умолчанию автоматически не создается;
  • конструктор копирования. Реализация по умолчанию выполняет поверхностное копирование объекта. Если выполняется работа с динамической памятью, то обязательно следует реализовать конструктор копирования явным образом;
  • «операторный» метод присваивания копированием. Реализация по умолчанию выполняет поверхностное копирование объекта. Если выполняется работа с динамической памятью, то обязательно следует реализовать метод явным образом;
  • конструктор перемещения. Если явным образом реализован конструктор копирования или метод присваивания копированием, то конструктор перемещения автоматически не создается. Если конструктора перемещения нет, то вызывается конструктор копирования;
  • «операторный» метод присваивания перемещением. Если явным образом реализован конструктор копирования или метод присваивания копированием, то метод присваивания перемещением автоматически не создается. Если метода присваивания перемещением нет, то вызывается метод присваивания копированием;
  • деструктор.

Все эти методы лучше реализовывать внутри класса явным образом или вместо реализации указать спецификаторы default или delete (см. разд. 13.8).

Учебник C++ (MinGW-W64)
Учебник C++ (MinGW-W64) в формате PDF

Помощь сайту

ЮMoney (Yandex-деньги): 410011140483022

ПАО Сбербанк:
Счет: 40817810855006152256
Реквизиты банка:
Наименование: СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК
Корреспондентский счет: 30101810500000000653
БИК: 044030653
КПП: 784243001
ОКПО: 09171401
ОКОНХ: 96130
Скриншот реквизитов