Вы уже порешали разные
задачи на move семантику, научились неплохо ей пользоваться,
но местами код мог получиться не очень простым, не очень компактным.
Давайте мы разберём одну конструкцию языка, которая позволяет сделать
использование move семантики более простым и более понятным читателю вашего кода.
Рассмотрим задачу SplitIntoSentences, в которой вам нужно было написать функцию,
которая принимает набор токенов и разбивает их на предложения.
Как выглядело решение, которое мы опубликовали?
Там есть функция, которая находит конец предложения,
и в целом она никак с move семантикой не связана, просто на алгоритмы и λ функции.
Её мы трогать сейчас не будем.
А есть, собственно, сама функция SplitIntoSentences,
которую вы видите на экране, в которой что происходит?
В которой мы заводим некоторый итератор tokens_begin и идём этим итератором по
токенам.
Мы находим конец предложения очередного,
и теперь именно здесь у нас move семантика используется, в этих пяти строчках.
Мы берём текущий токен, идём этим итератором от текущего
токена до конца предложения и все эти токены вставляем в текущее предложение,
а затем это предложение вставляем в вектор предложений.
Давайте посмотрим, в первую очередь, на вот этот цикл.
Этот цикл делает следующее: он перебирает все токены от одного до другого
(просто в диапазоне токенов) и в каждый из них вставляет вектор.
Зачем мы для этого писали цикл?
Казалось бы, у нас для этого некоторые алгоритмы.
Вот если бы мы не знали про move семантику, как бы мы написали этот цикл?
Мы бы, наверное, написали что-нибудь такое.
Давайте я вот эту часть закомментирую.
Попробую написать что-нибудь попроще, без оглядки на move семантику.
Наверное, я бы взял и создал предложение.
Я напомню, что это, на самом деле, просто вектор.
Sentence от токен у вектора (ну вот это, правда, вектор).
У вектора есть конструктор по двум итераторам.
Соответственно, я мог бы создать предложение от диапазона токенов.
Диапазон токенов у нас следующий: tokens_begin и sentence_end.
Казалось бы, я создал просто предложение по диапазону токенов.
Но здесь, конечно, будут происходить копирования.
Копирования токенов из вот этого диапазона в предложение.
Но вспомните важную мысль нашего блока, одну из основных: что если
где-то в коде происходит копирование, то там же можно сделать и перемещение.
Так и здесь.
Вот в этом алгоритме, на самом деле, в конструкторе вектора происходит
копирование токенов; можно сделать там перемещение.
Как это сделать?
Мы подключим модуль итератор и после этого нам станет
доступна специальная функция, которую мы вызовем от итераторов.
Функция называется make_move_iterator.
Я вызываю её от этого итератора и от этого итератора.
Я получаю диапазон move итераторов.
Что такое move итератор, что возвращает функция make_move_iterator?
Она возвращает некоторую обёртку над итератором,
которая если мы из неё хотим скопировать объект, на самом деле, его перемещает.
По сути как функция move меняет семантику переменной, чтобы она вела себя как
временный объект, так и функция make_move_iterator меняет семантику
итератора так, чтобы при обращении к нему данные бы перемещались, а не копировались.
Соответственно, вот так можно проще создать предложения — без цикла.
Конструктором от двух move итераторов.
А дальше я хочу это предложение положить в вектор предложений.
Я мог бы написать sentences.push_back(move(sentence)) вот
так вот.
Но зачем я создал переменную sentence и тут же из неё помувал?
Наверное, я могу просто в push_back создать это предложение.
Вот как-нибудь
так.
Я непосредственно при вызове push_back создаю объект типа
sentence от token и внутри в него передаю move итератора.
Казалось бы, всё.
Давайте попробуем запустить unit тесты.
Вот unit тесты, которые исходно были даны в той задаче.
Давайте я запущу и посмотрю, что будет.
Код компилируется.
Он скомпилировался и что-то пошло не так.
Что же мы забыли в нашем коде?
Давайте я остановлю и посмотрю внимательно.
В этом цикле я не просто складывал токены в предложения,
я ещё и изменял итератор tokens_begin, который у меня един на все итерации.
А здесь я итератор tokens_begin изменить забыл.
Поэтому давайте я его изменю.
tokens_begin равно — каким должен стать
этот итератор в самом конце — sentence_end.
И теперь я снова скомпилирую и запущу код.
Код компилируется.
Он скомпилировался, запустился.
И мы видим, что тест прошёл.
Здесь, правда, есть некоторые сообщения от статического анализатора.
На самом деле, если их удалить и код перекомпилировать, то всё будет хорошо.
Итак.
Мы познакомились с move итераторами,
которые получаются с помощью функции make_move_iterator.
Давайте я вот этот старый код окончательно удалю.
И рассмотрим ещё один пример, ещё одну задачу.
Задача «Читалка Иосифа».
В ней нужно было реализовать довольно замысловатый алгоритм,
но суть его была в том, что на вход приходит некоторый диапазон элементов,
и эти элементы нужно как-то в этом диапазоне переставить.
И как мы там использовали move семантику?
Мы вот здесь, в этом коде,
когда мы уже набрали наш вектор permutation,
мы из этого вектора элементы перемещаем в диапазон, начиная с range_begin.
То есть в исходный диапазон.
Опять же, как это можно записать без цикла с помощью алгоритмов?
Можно было бы написать алгоритм copy; я, по сути,
хочу скопировать из вектора permutation (begin от permutation,
end от permutation) в range_begin.
Я мог бы написать вот так,
закомментировав исходный код.
Дальше я могу заметить, что я теперь хочу
перемещать оттуда, поэтому хорошо бы вызвать
make_move_iterator и получить что-то вот такое.
[ШУМ] Вызвал make_move_iterator от begin,
make_move_iterator от end и range_begin.
От range_begin вызывать make_move_iterator не нужно,
потому что это диапазон, в который мы записываем данные.
make_move_iterator имеет смысл вызывать только для тех итераторов,
из которых данные будут забираться, то есть копироваться или перемещаться.
Итак.
Я вызвал copy с make_move_iterator.
Давайте я запущу unit тесты на эту функцию.
Код компилируется.
И всё в порядке.
А теперь давайте ещё один важный момент обсудим.
Если вы хотите вызвать copy от move итераторов (у вас есть какие-то итераторы,
вы хотите их обернуть в make_move_iterator и вызвать от них copy), на самом деле,
можно сделать короче и вызвать сразу алгоритм move.
Это, по сути, некоторый такой «брат» алгоритма copy,
который будет данные не копировать, а перемещать и избавит вас
от необходимости вызывать функцию make_move_iterator.
Соответственно, здесь можно написать move,
а вот эти вот обёртки make_move_iterator удалить.
Я просто говорю: «Перемести мне объекты из диапазона begin от permutation —
end от permutation в range_begin».
Давайте я уберу всё лишнее и ещё раз запущу unit тесты.
И — да, у нас всё в порядке.
Итак.
В этом видео мы узнали, что если есть некоторый алгоритм,
который копирует данные из, например, входного диапазона, то можно заставить
его эти данные перемещать, обернув итераторы в make_move_iterator.
Получатся некоторые move итераторы, которые в целом применимы во всех случаях,
когда алгоритм может копировать данные из входного диапазона.
И, кстати говоря, с помощью этих самых move итераторов можно, например,
ускорить задачу про сортировку слиянием, что мы и попросим вас сделать в задаче.