В предыдущем видео мы рассмотрели один из вариантов, когда передача временного объекта в метод позволяет нам ускорить программу. Но мы это рассмотрели только на примере тяжёлой строки и метода push_back у вектора. Давайте разберём ещё класс ситуаций, когда срабатывает перемещение, когда у нас не происходит копирование данных временного объекта. Итак, первое и, в общем, самое главное, что здесь нужно запомнить, что везде, где у вас может происходить копирование в языке C++, есть место и перемещению. И давайте вспомним, где вообще может происходить копирование объектов. Самый, на самом деле, простой случай — это присваивание. Скажем, у вас есть некоторая строка target_string, в которой есть некоторое значение old value. И есть некоторая строка, в которой более интересное значение — source_string. Ну вот, скажем, результат вызова функции MakeString. И мы хотим старое значение в строке target_string перезаписать новым значением. Мы это делаем с помощью оператора присваивания, просто с помощью символа «равно». target_string = source_string. И вот в таком варианте здесь будет происходить копирование. И опять же, давайте посмотрим на этот код и задумаемся, зачем же мы заводили переменную source_string, если мы просто хотим результат вызова функции MakeString положить в переменную target_string? Оказывается, и здесь тоже можно убрать вот эту промежуточную переменную и тем самым сэкономить ресурсы на копирование. Давайте обернём всё в макрос LOG_DURATION и проверим, помогает ли. Вот этот с промежуточной переменной присваивания. И попробуем без промежуточной переменной то же самое. Просто MakeString напишем справа от символа «равно». И короче, и, по идее, должно быть быстрее. Давайте сравним. Итак, мы результат вызова функции MakeString присваиваем в переменную target_string. И действительно код ускоряется примерно в два раза, что мы и видели до этого на примере push_back. Сэкономили на копировании. Где ещё может происходить копирование? Мы до этого добавляли в вектор, но есть и другие контейнеры. Например, множество, а у множества есть метод insert. Может ли метод insert вызывать перемещение? Может ли метод insert, принимая временный объект, оптимизировать его копирование, просто перекладывая указатель? Да, давайте попробуем. Опять же сразу макрос LOG_DURATION. Добавление в множество методом insert с промежуточной переменной сейчас будем пробовать. Давайте создадим множество, множество строк. strings Опять же, используем промежуточную переменную heavy_string и кладём в неё MakeString. И теперь в множество strings с помощью метода insert добавляем переменную heavy_string. Давайте опять же продемонстрируем, что вот здесь промежуточная переменная только замедляет ход; отказываемся от неё во втором варианте. Пробуем обойтись без промежуточной переменной. Берём вот это выражение MakeString и напрямую передаём в метод insert. Проверяем, стало ли быстрее. Код компилируется, запускается. И опять же стало примерно в два раза быстрее. Какой ещё есть интересный контейнер, который хранит в себе данные и который, возможно, может забирать данные временных объектов? Это, конечно же, словарь. Он интересен тем, что у него есть не просто какие-то объекты, которые в нём хранятся (как, например, вектор: есть вектор строк и всё, там строки хранятся, есть множество строк — там хранятся строки), а у словаря есть ключи и есть значения. И оказывается, он может перемещать в себе данные как для ключей, так и для значений. Поэтому давайте напишем ещё более интересный пример со словарём. Опять же макрос LOG_DURATION. Будем использовать оператор «квадратные скобки» для словаря, который вам так привычен, с промежуточными переменными и для ключа, и для значения. Давайте я создаю словарь новый. Из строк в строки для простоты. Назову его тоже strings, например. Создаю переменную для ключа, переменную для значения и добавляю это соответствие «ключ-значение» в словарь. Будет переменная ключ, там тоже будет MakeString. Тоже со значением value. И теперь давайте вот это соответствие «ключ-значение» добавим в наш словарь strings. strings от key равно value. Вот мы сейчас попробовали это сделать с промежуточными переменными. Давайте посмотрим, сколько это будет работать для начала. И потом будем пытаться ускорить. Это работает аж 470 миллисекунд. Хорошо. Давайте я попробую избавиться для начала от одной промежуточной переменной, а именно от переменной для значения. Я просто вот это выражение MakeString напишу справа от символа равно при добавлении соответствия «ключ-значение». А для ключа пока оставлю промежуточную переменную. Проверю, ускорилось ли так. И ускорилось ну примерно на четверть. Хорошо, давайте я откажусь от промежуточной переменной и для ключа тоже. Вот здесь давайте я скажу, что здесь была переменная для ключа, а теперь вообще не будет промежуточных переменных. Я сразу вот этот MakeString использую и в квадратных скобках тоже. Вот что может показаться неожиданным, что действительно квадратные скобки для словаря тоже могут копировать себе данные. Ну это естественно; на самом деле, квадратные скобки, если вы помните, для словаря не только позволяют найти этот ключ, но и добавить его, если его не было. В данном случае у нас словарь пустой, поэтому, конечно, данные для этого ключа внутри словаря нужны. Поэтому тут всё вполне валидно, и мы можем ожидать перемещения данных этого временного объекта. Давайте проверим, получилось ли код ускорить ещё сильнее. И похоже, что получилось. Исходный вариант работал 450 миллисекунд, ускоренный только для значения работал 300 миллисекунд, и когда мы ещё отказались от промежуточной переменной для ключа, он стал работать 200 миллисекунд. Итак. Мы видим, что, по сути, все методы контейнеров, которые забирали себе данные, которые их копировали в обычной ситуации, принимая временные объекты, эти данные себе перемещают, и никакого копирования не происходит. Ну и давайте для полноты картины поработаем не только со строками, но и, например, с векторами. Что мы увидели в предыдущем видео? Что если у объекта есть данные в куче, тогда эти данные могут переместиться, не копироваться, если объект временный. Ну и у каких контейнеров есть данные в куче? Конечно, не только у строк, но и, например, у векторов. Давайте мы напишем функцию MakeVector, которая создаст нам тяжёлый вектор по аналогии с функцией MakeString и попробуем его куда-нибудь положить. Допустим, это будет вектор int, пишем функцию MakeVector, которая возвращает этот вектор ну, скажем, из тех же ста миллионов интов. Неважно, чего; например, там нули будут. Ну и давайте попробуем, скажем, во множество положить этот вектор. Я прямо беру пример со строкой и исправляю его под вектор. Тут будет с вектором, тут тоже будет с вектором, будет множество векторов. Вот такое вот. Назовём его vectors. Здесь то же самое множество векторов. Давайте здесь прямо так и напишем: вектор от int heavy_vector равно MakeVector. Здесь мы пишем vectors точка insert от heavy_vector. И во второй ситуации то же самое. vectors точка insert от MakeVector. Давайте проверим, правда ли компилятор умеет оптимизировать копирование временного вектора, превращая его в перемещение. Итак. У нас работают все-все-все наши примеры. Дождались. И вот, в частности, есть пример с добавлением вектора во множество с помощью метода insert. И с промежуточной переменной он работал больше секунды, а без промежуточной переменной он работал почти 400 миллисекунд. Итак. В этом видео мы обсудили, что везде, где есть место копированию в языке C++, может происходить и перемещение. В каких случаях происходит копирование? При инициализации объекта каким-то другим объектом (как правило, тяжёлым), при присваивании в уже существующий объект данных из другого объекта и при вызове методов контейнеров, которые хотят положить данные своих аргументов в сам контейнер. Ну и таких методов, конечно, довольно много. У вектора это push_back, insert, у deque помимо push_back и insert это ещё и push_front, это метод insert у множества, квадратные скобки у словаря. Ну и вообще говоря, все остальные методы, которые хотят скопировать себе данные. Итак, важное правило. Если у вас есть некоторый временный объект, вы его создали вызовом функции или вызовом конструктора, старайтесь не потерять вот это его важное свойство — его временность. Поскольку если вы будете передавать этот временный объект как есть в те ситуации, в те выражение, где обычно происходит копирование, то на копировании компилятор сэкономит.