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

We use cookies. Read the Privacy and Cookie Policy

r-значения — это обычно временные объекты, поэтому их можно спокойно модифицировать; если известно, что параметр функции — r-значение, то его можно использовать как временную память, то есть «позаимствовать» его содержимое без ущерба для корректности программы. Это означает, что вместо копирования параметра, являющегося r-значением, мы можем просто переместить его содержимое. В случае больших динамических структур это позволяет сэкономить на выделении памяти и оставляет простор для оптимизации.

Рассмотрим функцию, которая принимает в качестве параметра std::vector<int> и хочет иметь его внутреннюю копию для модификации, так чтобы не затрагивать оригинал. Раньше мы для этого должны были принимать параметр как const-ссылку на l-значение и делать внутреннюю копию:

void process_copy(std::vector<int> const& vec_) {

 std::vector<int> vec(vec_);

 vec.push_back(42);

}

При этом функция может принимать как l-значения, так и r-значения, но копирование производится всегда. Однако, если добавить перегруженный вариант, который принимает ссылку на r-значение, то в этом случае можно будет избежать копирования, поскольку нам точно известно, что оригинал разрешается модифицировать:

void process_copy(std::vector<int>&& vec) {

 vec.push_back(42);

}

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

Листинг А.1. Класс с перемещающим конструктором

class X {

private:

 int* data;

public:

 X() : data(new int[1000000]) {}

 ~X() {

  delete [] data;

 }

 X(const X& other) : ← (1)

  data(new int[1000000]) {

  std::copy(other.data, other.data + 1000000, data);

 }

 X(X&& other) : ← (2)

  data(other.data) {

  other.data = nullptr;

 }

};

Копирующий конструктор(1) определяется как обычно: выделяем новый блок памяти и копируем в него данные. Но теперь у нас есть еще один конструктор, который принимает ссылку на r-значение (2). Это перемещающий конструктор. В данном случае мы копируем только указатель на данные, а в объекте other остается нулевой указатель. Таким образом, мы обошлись без выделения огромного блока памяти и сэкономили время на копировании данных из r-значения.

В классе X перемещающий конструктор — всего лишь оптимизация, но в ряде случаев такой конструктор имеет смысл определять, даже когда копирующий конструктор не предоставляется. Например, идея std::unique_ptr<> в том и заключается, что любой ненулевой экземпляр является единственным указателем на свой объект, поэтому копирующий конструктор лишен смысла. Однако же перемещающий конструктор позволяет передавать владение указателем от одного объекта другому, поэтому std::unique_ptr<> можно использовать в качестве возвращаемого функцией значения — указатель перемещается, а не копируется.

Чтобы явно переместить значение из именованного объекта, который больше заведомо не будет использоваться, мы можем привести его к типу r-значения либо с помощью static_cast<X&&>, либо путем вызова функции std::move():

X x1;

X x2 = std::move(x1);

X x3 = static_cast<X&&>(x2);

Это особенно удобно, когда требуется переместить значение параметра в локальную переменную или переменную-член без копирования, потому что хотя параметр, являющийся ссылкой на r-значение, и может связываться с r-значениями, но внутри функции он трактуется как l-значение:

void do_stuff(X&& x_) {

 X a(x_); ← Копируется

 X b(std::move(x_)); ← Перемещается

}              │ r-значение связывается

do_stuff(X());←┘ со ссылкой на r -значение

X x;         │ Ошибка, l -значение нельзя связывать

do_stuff(x);←┘ со ссылкой на r -значение

Семантика перемещения сплошь и рядом используется в библиотеке Thread Library — и в случаях, когда копирование не имеет смысла, но сами ресурсы можно передавать, и как оптимизация, чтобы избежать дорогостоящего копирования, когда исходный объект все равно будет уничтожен. Один пример мы видели в разделе 2.2, где std::move() использовалась для передачи экземпляра std::unique_ptr<> новому потоку, а второй — в разделе 2.3, когда рассматривали передачу владения потоком от одного объекта std::thread другому.

Ни один из классов std::thread, std::unique_lock<>, std::future<>, std::promise<>, std::packaged_task<> не допускает копирования, но в каждом из них имеется перемещающий конструктор, который позволяет передавать ассоциированный ресурс другому экземпляру и возвращать объекты этих классов из функций. Объекты классов std::string и std::vector<> можно копировать, как и раньше, но дополнительно они обзавелись перемещающими конструкторами и перемещающими операторами присваивания, чтобы избежать копирования данных из r-значений.

Стандартная библиотека С++ никогда не делает ничего с объектом, который был явно перемещён в другой объект, кроме его уничтожения или присваивания ему значения (путем копирования или, что более вероятно, перемещения). Однако рекомендуется учитывать в инвариантах класса состояние перемещен-из. Например, экземпляр std::thread, содержимое которого перемещено, эквивалентен объекту std::thread, сконструированному по умолчанию, а экземпляр std::string, бывший источником перемещения, все же находится в согласованном состоянии, хотя не дается никаких гарантий относительно того, что это за состояние (в терминах длины строки или содержащихся в ней символов).