[БЕЗ_ЗВУКА] В прошлом видео мы обсудили конструктор копирования и оператор присваивания и поняли, какой же действительно вызывается код, когда мы копируем какой-то объект в разных ситуациях. С другой стороны, когда мы обсуждали Move семантику, мы отмечали, что везде, где происходит копирование, должно быть место и для перемещения. Должна быть возможность для временного объекта получить там перемещение или вызвать функцию move и получить перемещение даже для постоянного объекта. Так вот, если мы объединим идею предыдущего видео вот с этой идеей, то получим понимание того, что должен быть какой-то конструктор перемещения и, наверное, оператор присваивания с какой-то копирующей версией. Да они есть. И если вот, например, мы рассмотрим такую ситуацию, когда мы инициализируем какой-то новый вектор выражением move от другого вектора, тогда вызывается конструктор от вот этого выражения move от source, и, поскольку это выражение ведет себя как временный объект, то здесь вызывается конструктор перемещения, специальный конструктор. Или, если мы, допустим, просто имеем некий вектор векторов и добавляем туда пушбэком временный вектор, то, конечно, тоже вызовется специальный конструктор перемещения. Вы можете заметить, что вот здесь, в верхней части слайда, нет случая, когда я инициализирую новый вектор временным вектором. И это, действительно, некоторый особый случай. В этом случае компилятор все разруливает еще лучше, и здесь даже конструктора перемещения не будет, и это мы обсудим попозже в нашем блоке. А пока давайте вернемся к оператору присваивания. Если мы выполняем присваивание, и в правой части этого присваивания у нас некоторый временный объект или move от некоторого постоянного объекта, тогда будет вызываться специальный оператор перемещающего присваивания. Давайте мы посмотрим на примере SimpleVector, как же должны выглядеть конструктор перемещения и перемещающий оператор присваивания. Для начала, вот у нас есть некоторый SimpleVector, у которого уже есть конструктор копирования и оператор присваивания. Они, в общем, работают, они уже написаны. Давайте я сейчас, пока не дописывая ничего про перемещения, попробую в функции main сделать что-нибудь такое: создаю SimpleVector source размера 1, и после этого создаю SimpleVector target, инициализирую его выражением move от source. Нам хотелось бы, чтобы source сейчас переместился в target, и после этого target имел бы размер 1, a source — 0. Это то, чего мы ожидаем от любого нормального контейнера C++. Давайте посмотрим, после этой операции какой размер имеет source и какой размер имеет target. Посмотрим, что вообще произошло. Ну, казалось бы, класс наш, и почему бы source мог бы опустеть? И, действительно, у меня оба эти вектора имеют размер 1. То есть никакого перемещения не случилось, случилось копирование. Если у вас есть ваш собственный класс, у которого уже есть конструктор копирования, и вы ничего не сказали про перемещение, то компилятор делает перемещение эквивалентным копированию. Опять же, в случае, если конструктор копирования вы написали сами. Это означает, что у вас какой-то интересный класс, и лучший компилятор делает перемещение эквивалентным копированию. Хорошо. Давайте попробуем сказать компилятору сгенерировать конструктор перемещения самостоятельно. По аналогии с конструктором копирования компилятор может сам сгенерировать конструктор перемещения, и он будет перемещать все поля. И мы можем специальной конструкцией попросить компилятор самостоятельно сгенерировать конструктор перемещения. Для этого мы пишем заголовок конструктора перемещения, и он выглядит вот таким образом. Это конструктор, котрый принимает хитрую ссылку. Эта ссылка, во-первых, не константная, а, во-вторых, у нее два амперсанда. Два амперсанда. И это не ссылка на ссылку. И это не какая-то очень-очень хитрая специальная ссылка. Это так называемая rvalue-ссылка. rvalue reference. Она ведет себя абсолютно так же, как ссылка, но позволяет принимать временные объекты. Мы чуть позже обсудим подробнее, как с ней себя вести, а пока мы просто объявили вот такой конструктор, конструктор перемещения, и вот сейчас попросим компилятор сгенерировать его самостоятельно. Напишем = default. И посмотрим, что теперь будет происходить в функции main. Мы ожидаем перемещение, мы ждем, что теперь вызовется конструкция перемещения, и посмотрим, что произойдет. Мы видим, что размеры 1 у source и у target, и опять программа упала в самом конце на деструкторах. Опять же, у нас два объекта владеют одними и теми же данными, ничего хорошего не получилось. Придется нам самостоятельно реализовывать конструктор перемещения. Хорошо. Давайте = default я уберу и напишу конструктор перемещения. Заодно разберемся, как же нам работать с этими хитрыми ссылками. Итак, конструктор перемещения принимает не константную rvalue-ссылку. Это не ссылка на ссылку, это rvalue-ссылка, которая привязывается к временному объекту. И, если мне нужно проинициализировать себя данными временного объекта, я могу не вызывать new, я могу просто взять other.data и этим указателем проинициализировать свой указатель. Но если я все оставлю вот так, это будет то же самое, что конструктор перемещения, который генерировал мне компилятор, и код все равно будет падать. Давайте я это продемонстрирую. У меня снова 1 1, и код падает. Чего же мне не хватает? Мне не хватает вот того самого, о чем я говорил в начале этого блока. Нам нужно забрать данные у объекта источника. То есть он больше не должен владеть этими данными. Как этого добиться? Нужно написать other.data = nullptr. Сказать: «Все, other, у тебя данных твоих больше нет. Ты готов их был отдать, я их у тебя забрал, и у тебя большего ничего нет. У тебя data nullptr — данных нет, ну, и размер и capacity нулевые». Все, вот теперь мы сделали хорошо. Мы теперь сказали, что у other данных больше нет, теперь ими владеем мы. И все. У исходного объекта source, из которого мы перемещали, размер теперь — 0, а у того объекта, в который мы перемещали, размер — 1. Все прекрасно. И заметьте, узнав, как на самом деле реализуется конструктор перемещения для контейнеров, мы поняли, почему эта самая ссылка не константная, почему нельзя переместить из константного объекта. Потому что нам здесь нужно произвести некоторые действия с этим объектом, из которого мы перемещаем. Если бы ссылка эта была константная, мы переместить бы не смогли. Ну, и давайте покажем, как должен выглядеть оператор присваивания. Там, в общем, ничего особенно интересного, но показать это нужно. Вот я беру оператор копирующего присваивания. Как от него будет отличаться оператор перемещающего присваивания? Он будет принимать rvalue-ссылку и будет реализован примерно так же, как конструктор перемещения. Вот давайте я возьму оператор присваивания. Сейчас я его найду. Вот он. Давайте я напишу оператор перемещающего присваивания, который принимает rvalue-ссылку. Что здесь нужно делать? Здесь, конечно, текущие данные нашего объекта нам не нужны, поэтому delete от data мы оставляем, но выделять новую память нам не нужно. Мы берем указатель из объекта other, его size, его capacity, а вместо копирования данных мы бы уже просто указатель переместили, на не нужно данные никакие копировать, нам нужно освободить other от владения его данными, поэтому здесь мы пишем то же самое, что писали в конструкторе перемещения. Пишем other.data = nullptr и other.size = other.capacity = 0. Ну, и давайте в функции main проверим конструктор перемещения. Давайте target создадим и проинициализируем с помощью конструктора от размера. А здесь запишем в него новое значение. Проверим, правда ли происходит перемещение при вызове оператора присваивания от move от source. Да. Все работает и даже не падает. Прекрасно. Итак, что же такое rvalue-ссылка? Rvalue-ссылка позволяет компилятору понять, какой из двух методов, один из которых принимает обычную ссылку, а другой — rvalue-ссылку, какой из двух методов надо вызывать. Если вы написали, или компилятор за вас сгенерировал два метода, один из которых принимает обычную ссылку, которая еще называется lvalue-ссылкой с одним амперсандом, а другой метод принимает rvalue-ссылку, то, если вы этот метод будете вызывать для временного объекта, вызовется его версия от rvalue-ссылки, а если вы будете его вызывать от постоянного объекта, то есть не от временного, то вызовется его версия от обычной ссылки. И так и происходит, например, с оператором присваивания. Если он есть и для обычной ссылки, и для rvalue-ссылки, то есть мы пишем target = source, например, то вызывается оператор присваивания копирующий от обычной ссылки, а если мы пишем target = и справа временный объект или move, то у нас происходит перемещение, вызывается метод operator= от rvalue-ссылки. Для собственных типов данных компилятор сам сгенерирует за вас конструктор перемещения и перемещающие операторы присваивания, которые просто переместят все поля. Ну, точно так же, как это происходит для конструктора копирования и копирующего оператора присваивания. Поэтому, если вы пишете свой тип данных, котрый сам не управляет памятью, у которого просто есть какие-то поля, в частности, например, строки, векторы и так далее, то для них перемещение будет просто работать, потому что компилятор сам сгенерирует все, что нужно.