Урок №192. Функция std::move()

  Юрий  | 

  |

  Обновл. 15 Сен 2021  | 

 61072

 ǀ   14 

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

Проблема

Очень часто объекты, с которыми вам придется работать, будут не r-values, а l-values. Например, рассмотрим следующий шаблон функции swap():

Принимая два объекта типа T (в данном случае std::string) функция swap() меняет местами их значения, делая при этом три копии.

Следовательно, результат выполнения программы:

x: Anton
y: Max
x: Max
y: Anton

А как мы уже узнали на предыдущем уроке, копирование — это не очень эффективно. А поскольку мы выполняем копирование трижды, то это еще и сравнительно медленно. Мы можем этого избежать. Наша цель — поменять местами значения x и y. А для этого мы можем использовать перемещение вместо копирования, сделав наш код более производительным!

Но как? Проблема состоит в том, что параметры x и y являются ссылками l-value, а не ссылками r-value, поэтому у нас нет способа вызвать конструктор перемещения или оператор присваивания перемещением вместо конструктора копирования и оператора присваивания копированием. По умолчанию у нас используется семантика копирования. Что делать?

Функция std::move()


Функция std::move() — это стандартная библиотечная функция, которая конвертирует передаваемый аргумент в r-value. Мы можем передать l-value в функцию std::move(), и std::move() вернет нам ссылку r-value. Для работы с std::move() нужно подключить заголовочный файл utility.

Вот вышеприведенная программа, но уже с функцией swap(), которая использует std::move() для преобразования l-values в r-values, чтобы мы имели возможность использовать семантику перемещения вместо семантики копирования:

Результат выполнения один и тот же:

x: Anton
y: Max
x: Max
y: Anton

Но эта версия программы гораздо эффективнее. При инициализации tmp, вместо создания копии x, мы используем std::move() для конвертации переменной x, которая является l-value, в r-value. А поскольку параметром становится r-value, то с помощью семантики перемещения x перемещается в tmp.

Затем, спустя несколько дополнительных перестановок, значение переменной x перемещается в переменную y, а значение y перемещается в переменную x.

Еще один пример

Мы также можем использовать std::move() для заполнения контейнерных классов (таких как std::vector) значениями l-values.

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

Результат выполнения программы:

Copying str
str: Bye
vector: Bye

Moving str
str:
vector: Bye Bye

В первом случае мы передаем l-value в push_back(), поэтому используется семантика копирования для добавления элемента в вектор. По этой причине переменная str остается с прежним значением.

Во втором случае мы передаем r-value (фактически l-value, которое конвертируется в r-value через std::move()) в push_back(), поэтому используется семантика перемещения для добавления элемента в вектор. Это более эффективно, так как элемент вектора может украсть значение переменной std::string, вместо его копирования. По этой же причине str лишается своего значения.

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

Функции перемещения


Как мы уже говорили на предыдущем уроке, вы должны оставлять объекты, ресурсы которых вы перемещаете, в четко определенном состоянии. В идеале это должно быть «нулевое состояние» (null/nullptr).

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

В примере, приведенном выше, строка str остается пустой после выполнения перемещения (что всегда делает std::string после успешного перемещения). Таким образом, мы можем повторно её использовать, если, конечно, захотим, или проигнорировать, если она нам больше не нужна.

Чем еще полезна функция std::move()?

Функция std::move() также может быть полезна при сортировке элементов массива. Многие алгоритмы сортировки (такие как «метод выбора» и «сортировка пузырьком») работают путем замены целых пар элементов. На предыдущих уроках нам приходилось использовать семантику копирования для выполнения таких замен. Теперь же мы можем использовать семантику перемещения, которая эффективнее.

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

Заключение


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

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (162 оценок, среднее: 4,93 из 5)
Загрузка...

Комментариев: 14

  1. Алексей:

    Добавил const в пример, и почему то код выполнился… Можете пояснить почем?

    1. Steindvart:

      Очень странно. Такое не должно компилироваться и у меня не компилируется — использую MVS.

    2. Anno:

      У тебя std::swap вызывается. Измени имя функции и ты получишь сообщение об ошибке.

      1. Максим:

        Вот так подстава с этим std::swap.
        У меня тоже скомпилировалось.
        До этого момента думал, что если избегать "using namespace std;", то компилятор будет четко различать пользовательские функции (swap()) от функций из стандартной библиотеки (std::swap()).

        1. Алексей:

          Кто-нибудь может объяснить почему так получается? Как в таком случае использовать собственный одноименный метод swap?

        2. Kayfor:

          "Кто-нибудь может объяснить почему так получается? Как в таком случае использовать собственный одноименный метод swap?+"

          По этой ссылке можно ознакомиться с реализацией std::swap : http://www.cplusplus.com/reference/algorithm/swap/

          Если она вас не устраивает — можете сделать собственный аналог.
          Запилив свой вариант в статичном классе, явно вызывая по имени этого класса свой swap вы сможете использовать собственную реализацию.

        3. Aleksandr:

          Если вам требуется написать функцию с тем же именем, что уже есть в стандартной библиотеке, определите собственный namespace и пишите в нем, для ее вызова необходимо будет указать явно указать из какого она пространства имен (your_namespace::)

      2. Alex:

        Потому что это шаблон, шаблоны не компилируются если нету экземпляров. Создай экземпляр шаблона и выйдет ошибка.

    3. Alex:

      Потому что это шаблон, шаблоны не компилируются если нету экземпляров. Создай экземпляр шаблона и выйдет ошибка.

    4. Surprizze:

      В данном случае вызывается

      Если вы вызовите свою версию , при помощи глобального пространства имен:

      то получите ошибку компилятора

  2. Анастасия:

    А поясните, пожалуйста, почему для приведённых примеров срабатывает оператор присваивания перемещением? Он встроен в std::vector и std::string? Мы сами его не создавали, а в прошлом уроке было правило, что если мы хотим, чтобы срабатывало именно перемещение, надо самим об этом позаботиться.

    1. Kotik:

      Правило «Если вам нужен конструктор перемещения и оператор присваивания перемещением, которые выполняют перемещение (а не копирование), то вам их нужно предоставить (написать) самостоятельно» относится только к тем объектам, что проектируем мы сами.

      1. Anonym:

        Есть правило Пяти:
        Если вам из 5 методов нужно что-то реализовать, то тогда уж надо реализовать остальные методы из пятерки:
        Деструктор
        Конструктор копирования
        Конструктор перемещения
        Присваивание копированием
        Присваивание перемещением

        Логика тут в том, что для простых типов пойдет лишь простой конструктор (которой инициализирует ваши поля, например).
        А Пятерку компилятор уже неявно соберет сам (Т.е. соберет эти специализированные методы по-умолчанию).

  3. kmish:

    Если изменить последний пример предыдущего урока следующим образом (напишу только измененные строчки):

    то есть, передать объект в функцию cloneArrayAndDouble() не по константной ссылке l-value, а по ссылке r-value. И замерить время выполнения, то при передаче по r-value время немного дольше. В среднем 0,005 против 0,0065 в пользу l-value на моей машине.
    Из чего можно сделать вывод, что если есть возможность выбрать, как мы передаем, то лучше передавать const DynamicArray<int> &arr, т.е. по константной ссылке l-value.

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *