Что делает движущиеся объекты быстрее, чем копирование?

Я слышал, как Скотт Майерс сказал: "std::move() ничего не двигает"... но я не понял, что это значит.

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

class Box { /* things... */ };

Box box1 = some_value;
Box box2 = box1;    // value of box1 is copied to box2 ... ok

Как насчет:

Box box3 = std::move(box1);

Я понимаю правила lvalue и rvalue, но то, что я не понимаю, это то, что на самом деле происходит в памяти? Это просто копирование значения по-разному, разделение адреса или что? В частности: что делает перемещение быстрее, чем копирование?

Я просто чувствую, что понимание этого сделает все ясным для меня. Спасибо заранее!

EDIT: Обратите внимание, что я не спрашиваю о реализации std::move() или каких-либо синтаксических материалах.

Ответ 1

Как уже сообщал @gudok, все в реализации... Тогда бит находится в коде пользователя.

Реализация

Предположим, что мы говорим о конструкторе-копии, чтобы присвоить значение текущему классу.

Реализация, которую вы предоставите, будет учитывать два случая:

  • параметр является l-значением, поэтому вы не можете его коснуться, по определению
  • параметр является r-значением, поэтому, неявно, временное не будет жить намного дольше, чем вы его используете, поэтому вместо копирования его содержимого вы можете украсть его содержимое

Оба реализованы с использованием перегрузки:

Box::Box(const Box & other)
{
   // copy the contents of other
}

Box::Box(Box && other)
{
   // steal the contents of other
}

Реализация для легких классов

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

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

Предложение только реализации значения l будет более чем достаточно.

Реализация для более тяжелых классов

Но в случае некоторых тяжелых классов (т.е. std::string, std:: map и т.д.) копирование подразумевает потенциальную стоимость, обычно в распределениях. Поэтому, в идеале, вы хотите избежать его как можно больше. Здесь кражу данные из временных страниц становятся интересными.

Предположим, что в вашем ящике содержится необработанный указатель на HeavyResource, который является дорогостоящим для копирования. Код будет выглядеть следующим образом:

Box::Box(const Box & other)
{
   this->p = new HeavyResource(*(other.p)) ; // costly copying
}

Box::Box(Box && other)
{
   this->p = other.p ; // trivial stealing, part 1
   other.p = nullptr ; // trivial stealing, part 2
}

Этот простой конструктор (конструктор-копир, которому требуется выделение) намного медленнее другого (конструктор move, требующий только назначения необработанных указателей).

Когда безопасно "красть"?

Дело в том, что по умолчанию компилятор будет вызывать "быстрый код" только тогда, когда параметр является временным (он немного более тонкий, но носит со мной...).

Почему?

Поскольку компилятор может гарантировать, что вы можете украсть у какого-либо объекта без каких-либо проблем только, если этот объект является временным (или будет уничтожен вскоре после этого). Для других объектов кража означает, что у вас внезапно есть "пустой" или "неполный" объект, который еще может использоваться в коде далее. Преодоление сбоев или ошибок:

Box box3 = static_cast<Box &&>(box1); // calls the "stealing" constructor
box1.doSomething();         // Oops! You are using an "empty" object!

Но иногда вам нужна производительность. Итак, как вы это делаете?

Код пользователя

Как вы писали:

Box box1 = some_value;
Box box2 = box1;            // value of box1 is copied to box2 ... ok
Box box3 = std::move(box1); // ???

Что происходит для box2, так это то, что, поскольку box1 является значением l, вызывается первый, медленный экземпляр-конструктор. Это нормальный код С++ 98.

Теперь, для box3, происходит что-то смешное: std:: move возвращает один и тот же box1, но в качестве ссылки r-value вместо l-значения. Итак, строка:

Box box3 = ...

... НЕ будет вызывать экземпляр-конструктор на поле1.

Он вызовет INSTEAD конструктор кражи (официально известный как move-constructor) в box1.

И поскольку ваша реализация конструктора перемещения для Box делает "кражу" содержимого box1, в конце выражения box1 пуст, а box3 содержит (предыдущий) контент box1.

Конечно, запись std:: move на l-value означает, что вы обещаете, что не будете использовать это l-значение снова (если вы не добавите еще одно допустимое значение внутри него). Компилятор вам не поможет.

Заключение

Как сказано выше, std:: move ничего не делает. Он говорит только компилятору: "Вы видите, что l-значение? Пожалуйста, считайте его значением r всего на секунду".

Итак, в:

Box box3 = std::move(box1); // ???

... код пользователя (т.е. std:: move) сообщает компилятору, что этот параметр можно рассматривать как r-значение для этого выражения, и, таким образом, будет вызываться конструктор перемещения.

Для автора кода (и анализатора кода) код действительно говорит, что это нормально, чтобы украсть содержимое box1, чтобы переместить его в поле3. Затем автору кода необходимо убедиться, что box1 больше не используется в этом "пустом" состоянии. Это их ответственность.

Но в конце концов, реализация конструктора перемещения будет иметь значение, в основном в производительности: если конструктор перемещения фактически украл содержимое r-значения, вы увидите разницу. Если это что-то еще, тогда автор солгал об этом, но это еще одна проблема...

Ответ 2

Все о реализации. Рассмотрим простой класс строк:

class my_string {
  char* ptr;
  size_t capacity;
  size_t length;
};

Семантика копии требует от нас полной копии строки, включая выделение другого массива в динамической памяти и копирование там *ptr, что дорого.

Семантика перемещения требует от нас только переноса значения самого указателя на новый объект без дублирования содержимого строки.

Если, конечно, класс не использует динамическую память или системные ресурсы, то нет никакой разницы между перемещением и копированием с точки зрения производительности.

Ответ 3

Функция std::move() следует понимать как приведение к соответствующему типу rvalue, который позволяет перемещать объект вместо копирования.


Это может не иметь никакого значения:

std::cout << std::move(std::string("Hello, world!")) << std::endl;

Здесь строка уже была rvalue, поэтому std::move() ничего не изменила.


Он может включать перемещение, но может все же привести к копированию:

auto a = 42;
auto b = std::move(a);

Нет более эффективного способа создания целого числа, которое просто копирует его.


Если это приведет к тому, что произойдет переход, то аргумент

  • - это значение lvalue или lvalue,
  • имеет конструктор перемещения или оператор назначения перемещения, а
  • (неявно или явно) является источником конструкции или назначения.

Даже в этом случае это не сам move(), который фактически перемещает данные, это конструкция или присвоение. std:move() - это просто бросок, который позволяет это произойти, даже если у вас есть lvalue для начала. И переход может произойти без std::move, если вы начнете с rvalue. Я думаю, что смысл заявления Майерса.