[БЕЗ_ЗВУКА] В предыдущих видео мы познакомились с move-семантикой, научились ее использовать, научились, когда нужно не терять свойство временности объекта, научились использовать функцию move, порешали задачи и в общем-то все хорошо. Давайте сейчас посмотрим немного, как все работает под капотом. А именно в этом видео мы обсудим, что происходит в следующих строчках кода. Что происходит, если вы берете какой-то контейнер, например, вектор целых чисел, создаете вектор source, вектор target и инициализируете при создании вектор target вектором source. Или что происходит в ситуации, когда вы создали уже как-то source target, ну, скажем, еще какой-то у вас есть source 2, и в существующий вектор target записываете, присваиваете данные от source 2. Вот что происходит в таких ситуациях, какой именно код вызывается? Но перед тем, как это обсуждать, давайте все-таки зададимся вопросом о том, зачем нам это нужно. Какова мотивация вообще здесь понимания того, что происходит под капотом? Во-первых, нам важно понять, что нужно сделать с нашими собственными классами, которые мы пишем сами, чтобы для них такой код работал и работал корректно. Кроме того, важный момент, нам нужно научиться разговаривать с разработчиками на одном языке, чтобы мы друг друга понимали, и, в частности, могли читать документацию по методам контейнеров и, например, понимать, какие методы умеют перемещать свои аргументы. Ну и наконец узнав, как это всё реализовано, можно развить интуицию на тему move-семантики и копирований вообще. И можно даже, например, не зная, что константный объект не сможет переместиться, понимая, как оно реализовано, предположить, что константный объект не переместится. Это все очень полезно, поэтому давайте все-таки мы ответим на те вопросы, которые мы задали до этого. Итак. В том случае, когда мы создали некоторую переменную source, как-то ее проинициализировали и теперь хотим проинициализировать переменную target значением переменной source, или мы это сделаем не с помощью символа =, а с помощью вызова конструктора, в этом случае, как вы можете понять, вызовется некоторый специальный конструктор вектора от константной ссылки на другой вектор. Вот он выглядит вот так и он называется специально, называется конструктор копирования или copy constructor. Итак, просто специальный конструктор от константной ссылки на объект того же типа. Еще вариант, когда мы в уже существующей переменной target присваиваем значение некоторой переменной source. Вот, обратите внимание на третью строчку. Мы не создаем новый вектор target, инициализируя его переменной source, мы уже как-то его создали, и теперь передумали хранить там то, что мы хранили до этого, и хотим записать туда данные из вектора source. На самом деле вот такая строчка, третья, эквивалентна четвертой строчке, а именно вызову специального метода со специальным названием оператор равно. На самом деле это для вас вряд ли будет сюрпризом, потому что вы уже учились перегружать собственные операторы, и на самом деле равно — тоже оператор, который можно перегрузить. И он называется оператором присваивания, или оператором копирующего присваивания. И именно он вызывается, когда вы пишете target = source. Итак, давайте поближе посмотрим на эти специальные методы, на конструкцию копирования и оператора присваивания, на примере специального класса, который мы называем logger, который будет копировать вызовы этих методов, и просто убедитесь, что они действительно вызываются. Итак, у меня есть некоторый класс logger, у которого есть конструктор по умолчанию, в котором просто выводится сообщение default constructor. ctor это просто общепринятое сокращение для слова конструктор. Давайте я напишу теперь помимо конструктора по умолчанию еще и конструктор копирования. Он будет принимать константную ссылку на logger. Назовем ее other. И здесь просто будем выводить сообщения в copy ctor. Причем поскольку переменная other здесь не используется, ее просто можно удалить. И просто сказать, что я принимаю какую-то ссылку и ее не использую. Помимо этого давайте я напишу оператор присваивания. Пусть он возвращает void. Оператор равно принимает ту же ссылку на logger и будет выводить, что это оператор присваивания. copy assignment. И теперь давайте я вот эти самые разные ситуации, которые я показал на слайде, проверю. Давайте я создам logger source, logger target и logger target проинициализирую переменной source. Запущу код и посмотрю, какие конструкторы и какие операторы будут вызваться. У меня есть [НЕРАЗБОРЧИВО] где я не использую переменную target, ничего страшного. Главное, что я вижу, что у меня вызывается конструктор по умолчанию, вот здесь, для source, и конструктор копирования для target. Еще раз, я хочу создать переменную target с помощью переменной source. Вызывается конструктор копирования. Давайте я покажу копирование еще на одном примере, скажем, я создам вектор logger и положу туда с помощью метода push back logger. Итак, у меня будет вектор логгеров, loggers, и в него я положу переменную target, например, которая у меня уже есть. Посмотрим, какие еще операторы вызовутся. И я вижу, что у меня помимо тех вызовов конструктора по умолчанию, конструктора копирования, которые уже были, добавился еще один вызов конструктора копирования, это переменная target скопировалась внутрь вектора. Все вполне ожидаемо. Наконец давайте посмотрим на оператора присваивания. Для этого достаточно написать что-нибудь вроде source = target. Конечно, они все эти классы пустые и тут ничего интересного не происходит, но зато мы видим, какие операторы вызываются. Посмотрим, есть ли у нас вызов оператора присваивания, да, есть, все в порядке. Хорошо. Давайте теперь зададимся вопросом, что же происходит для собственных типов данных. Мы можем написать для них какие-то свои конструкторы. Но компилятор, на самом деле, сам, и даже если мы эти конструкторы, конструктор копирования и оператора присваивания, не написали, он сам их сгенерирует и они по умолчанию будут просто копировать все поля, которые у нашего собственного класса есть. Для каких же типов все же есть необходимость самим реализовывать конструктор копирования и оператор присваивания? На самом деле вы уже можете догадаться, какой ответ на этот вопрос. А именно, это надо делать для тех типов, у которых есть необходимость управлять данными самостоятельно, которые хранят указатель на некоторые данные, например, и сами вызывает new, сами вызывают delete. Например, это класс Simple Vector. Давайте для этого класса напишем конструктор копирования и тем самым поймём, что же там происходит на самом деле. Итак, посмотрим на Simple Vector наш, в котором есть конструктор по умолчанию, конструктор от размера, деструктор, в общем, все то, что у него уже было до этого, можем еще раз проглядеть реализации всех методов, тут ничего неожиданного. Давайте попробуем скопировать какой-нибудь Simple Vector. Заметьте, конструктора копирования сейчас тут нет. Посмотрим, получится у нас или нет. Почему бы и нет? Вот я создаю Simple Vector, скажем, из int, скажем, source, он имеет размер 1, и я положу в него что-нибудь, какую-нибудь восьмерку, затем я создам еще один Simple Vector и назову его target и тоже проинициализирую его вот так с помощью конструктора копирования. И после этого я выведу, что у меня лежит в нулевом элементе вектора source и что у меня лежит в нулевом элементе вектора target. Наверное, хотелось бы ожидать, что у меня получится две восьмерки и что случится копирование. Но вы уже наверное догадываетесь, что что-то пойдет не так, потому что этот класс самостоятельно управляет памятью. И просто копирование всех его полей нам здесь не подходит. Тем не менее, давайте скомпилируем и запустим код. Посмотрим, что же у нас получится. Итак, две восьмерки, и не успели мы посмотреть на эти две восьмерки, как программа упала. То есть она вроде бы выполнила весь свой код, все вроде бы скопировала, вывела две восьмёрки и после этого уже упала. Что у нас происходит в конце вызова любой функции? Вызываются деструкторы для тех объектов, которые у нас были. И действительно, у нас был объект source, который владел своими данными, у него был указатель на эти данные, и был объект target, у которого получился тот же самый указатель на те же самые данные. Эти два вектора владеют одними и теми же данными, и после этого и тот, и другой вектор захотел эти данные удалить. Ну и в общем-то всё, один их удалил, а другой уже не смог, потому что их уже нет, программа упала. Что же делать? Надо написать свой конструктор копирования. Давайте его напишем. Итак, вот у нас есть конструктор от размера. Давайте еще напишем конструктор от константной ссылки на Simple Vector. other. Заметьте, я мог бы здесь написать Simple Vector от T, но поскольку здесь T у объекта, который мы инициализируем, и у того, которым мы инициализируем, эти T одинаковые, я могу здесь это T не писать, просто написать Simple Vector. И давайте я напишу какой-нибудь код для реализации этого конструктора. Для примера скопирую конструктор от размера. Вот так. Теперь у меня конструктор от константной ссылки на Simple Vector от T. И что я здесь напишу? Вот здесь я не могу просто скопировать, я не могу написать вот так вот, data от other data. Мы не можем вместе с тем объектом, из которого я инициализируюсь, и вот как бы этот новый Simple Vector, мы не можем владеть одними и теми же данными. Поэтому надо здесь выделить новую память нужного размера. Новую память под other capacity объектов типа T. size проинициализировать полем size объекта other и capacity проинициализировать полем capacity объекта other. Итак, в конструкторе копирования я должен выделить новую память под новый объект нужного размера, проинициализировать остальные поля и, наконец, нужно заполнить эти данные. Например, можно вызвать алгоритм copy, сказав ему, откуда копировать, от other begin и other end, и куда копировать, в результат вызова begin для текущего объекта. Вот вы написали такой конструктор копирования, давайте посмотрим, как работает наша функция main, стало ли ей лучше. И, действительно, теперь у нас две восьмерки и код не падает. Нам удалось написать собственный конструктор копирования для класса Simple Vector. Итак, мы обсудили, что такое конструктор копирования и оператор присваивания, обсудили, что почти всегда достаточно тех конструкторов копирования и операторов присваивания, которые генерируются компилятором. И вам не нужно их писать самим, но если ваш класс самостоятельно управляет памятью, то вам нужно писать для него свой конструктор копирования, и мы это уже сделали на примере Simple Vector, и оператор присваивания. И мы предложим вам задачу, в которой для Simple Vector вам нужно будет самостоятельно написать оператор присваивания.