Skip to the content.

Семантика перемещения

Может оказаться что операция копирования (конструктор копирования или оператор присваивания) достаточно дорогая для определенного класса (например для String: посимвольное копирование) и можно обойтись простым перемещением.

Если конструктор копирования это “скопируй данные у этого объекта” (а сам объект не изменяй, поскольку он передается по константной ссылке), то перемещающий конструктор это “отбери данные у этого объекта”. Это и называется перемещение:

struct String
{
      String(String && other) : data(other.data), size(other.size) // отбираем данные
      {
          // оставляем "ограбленный" объект в согласованном состоянии
          other.size = 0;
          other.data = nullptr;
      }
};

Мы получаем объект по rvalue-ссылке потому, что если бы передали в конструктор lvalue-ссылку то вызвался бы конструктор копирования.

ВНИМАНИЕ! “Ограбленный” объект нужно всегда оставлять в согласованном состоянии, мы не можем его просто так бросить, как минимум, потому что когда-то у него вызовется деструктор, который почистит отобранную нами область памяти. Этот объект можно “занулить”, но лучше всего воспользоваться std::swap (о нём ниже).

Аналогично перемещающий оператор присваивания:

struct String
{
    Struct & operator=(String && other)
    {
        if (this != &other) // "обворовывать" самого себя - глупо
        {
            // освобождаем текущую область памяти
            delete [] data;
        
            // отбираем данные
            data = other.data;
            size = other.size;
            
            // оставляем в согласованном состоянии
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }
};

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

std::swap()

Очень удобно использовать std::swap() из заголовка #include <utility> для реализации перемещения (просто меняет значения двух объектов между собой):

#include <utility>

struct String
{
    // реализуем метод swap
    void swap(String & other)
    {
        std::swap(data, other.data);
        std::swap(size, other.size);
    }
    
    // перемещающий конструктор
    String(String && other)
    {
        // что бы после обмена было согласованное состояние
        data = nullptr;
        size = 0;
        
        swap(other);
    }
    
    // перемещающий оператор копирования
    String & operator=(String && other)
    {
        if (this != &other)
        {
            swap(other);
        }
        return *this;
    }
};

Однако теперь нужно следить в перемещающем конструкторе за согласованым состоянием после обмена, поскольку объект, который “обворовывает” еще не создан!

Когда используются перемещающие методы

Если передавать в них:

Примеры:

String a(String("Hello")); // временный объект -> rvalue -> перемещающий конструктор
String b(a); // конструктор копирования
String c(std::move()); // std::move() -> rvalue -> перемещающий конструктор
a = b; // оператор присваивания
b = std::move(c); // std::move() -> rvalue -> перемещающий оператор присваивания
c = String("world"); // временный объект -> перемещающий оператор присваивания

Эти правила справедливы и для любых других методов и функций в которых происходит перегрузка между rvalue-ссылками и lvalue-ссылками!

Особые методы

С появлением перемещающих методов, расширился список особых методов, которые компилятор умеет генерировать самостоятельно:

Однако правила генерации новых методов отличаются от старых: перемещающие методы генерируются только, если в классе отсутствуют другие особые методы либо с ключевым словом default.

Правила генерации других особых методов признаны устаревшими и могут быть изменены в будущем.