M.3 – Конструкторы перемещения и присваивание перемещением
В уроке «M.1 – Введение в умные указатели и семантику перемещения» мы рассмотрели std::auto_ptr , обсудили необходимость семантики перемещения и рассмотрели некоторые недостатки, которые возникают, когда функции, разработанные для семантики копирования (конструкторы копирования и операторы присваивания копированием) переопределяются для реализации семантики перемещения.
В этом уроке мы более подробно рассмотрим, как C++11 решает эти проблемы с помощью конструкторов перемещения и присваивания перемещением.
Конструкторы копирования и присваивание копированием
Во-первых, давайте сделаем обзор семантики копирования.
Конструкторы копирования используются для инициализации класса путем создания копии объекта того же класса. Присваивание копированием используется для копирования одного объекта класса в другой существующий объект класса. По умолчанию, если конструктор копирования и оператор присваивания копированием не указаны явно, C++ предоставляет их. Эти предоставляемые компилятором функции создают поверхностные копии, что может вызывать проблемы для классов, динамически выделяющих память. Таким образом, классы, которые имеют дело с динамической памятью, должны переопределять эти функции для создания глубоких копий.
Возвращаясь к нашему примеру класса умного указателя Auto_ptr из первого урока этой главы, давайте рассмотрим версию, которая реализует конструктор копирования и оператор присваивания копированием, которые делают глубокие копии, и пример программы, которая их проверяет:
В этой программе мы используем функцию с именем generateResource() для создания умного указателя, инкапсулирующего ресурс, который затем передается обратно в функцию main() . Затем функция main() присваивает его существующему объекту Auto_ptr3 .
Когда эта программа запускается, она печатает:
(Примечание: вы можете получить только 4 сообщения, если ваш компилятор исключает копирование возвращаемого значения из функции generateResource() )
Для такой простой программы происходит слишком много созданий и уничтожений объектов Resource ! Что тут происходит?
Давайте рассмотрим подробнее. В этой программе выполняется 6 ключевых шагов (по одному для каждого напечатанного сообщения):
- Внутри generateResource() создается локальная переменная res , которая инициализируется динамически размещаемым объектом Resource , что приводит к первому сообщению " Resource acquired ".
- res возвращается обратно в main() по значению. Здесь мы возвращаем по значению, потому что res – это локальная переменная, она не может быть возвращена по адресу или по ссылке, потому что будет уничтожена при завершении generateResource() . Таким образом, res копируется конструктором во временный объект. Поскольку наш конструктор копирования выполняет глубокое копирование, здесь выделяется новый Resource , что приводит ко второму сообщению " Resource acquired ".
- res выходит за пределы области видимости, уничтожая первоначально созданный Resource , что приводит к первому сообщению " Resource destroyed ".
- Этот временный объект присваивается mainres путем присваивания копированием. Поскольку наше присваивание копированием также выполняет глубокое копирование, размещается новый Resource , вызывая еще одно сообщение " Resource acquired ".
- Выражение присваивания завершается, и временный объект выходит за пределы области действия выражения и уничтожается, вызывая сообщение " Resource destroyed ".
- В конце main() переменная mainres выходит из области видимости, и отображается наше последнее сообщение " Resource destroyed ".
Короче говоря, поскольку мы вызываем конструктор копирования один раз, чтобы скопировать res во временный объект, и один раз присваивание копированием для копирования временного объекта в mainres , в итоге мы размещаем и уничтожаем в общей сложности 3 отдельных объекта.
Неэффективно, но, по крайней мере, не дает сбоев!
Однако с семантикой перемещения мы можем добиться большего.
Конструкторы перемещения и присваивание перемещением
C++11 определяет две новые функции, обслуживающие семантику перемещения: конструктор перемещения и оператор присваивания перемещением. В то время как цель конструктора копирования и присваивания копированием – выполнить копирование одного объекта в другой, цель конструктора перемещения и присваивания перемещением – передать владение ресурсами от одного объекта к другому (что обычно намного дешевле, чем создание копии).
Определение конструктора перемещения и присваивания перемещением работают аналогично их аналогам для копирования. Однако в то время как копирующие версии этих функций принимают в качестве параметра константную lvalue-ссылку, перемещающие версии этих функций используют в качестве параметра неконстантные rvalue-ссылки.
Вот тот же класс Auto_ptr3 , что и выше, с добавленными конструктором перемещения и оператором присваивания перемещением. Для сравнения мы оставили выполняющие глубокое копирование конструктор копирования и оператор присваивания копированием.
Конструктор перемещения и оператор присваивания перемещением просты. Вместо того, чтобы выполнять глубокое копирование исходного объект ( а ) в неявный объект this , мы просто перемещаем (крадем) ресурсы исходного объекта. Это включает в себя поверхностное копирование указателя исходного объекта в неявный объект this с последующей установкой для указателя исходного объекта значения nullptr .
При запуске эта программа печатает:
Это намного лучше!
Ход программы точно такой же, как и раньше. Однако вместо вызова конструктора копирования и оператора присваивания копированием эта программа вызывает конструктор перемещения и оператор присваивания перемещением. Рассмотрим немного подробнее:
- Внутри generateResource() создается локальная переменная res , которая инициализируется динамически размещаемым объектом Resource , что приводит к первому сообщению " Resource acquired ".
- res возвращается обратно в main() по значению. res перемещается конструктором во временный объект, передавая динамически созданный объект, хранящийся в res , во временный объект. О том, почему это происходит, мы поговорим ниже.
- res выходит из области видимости. Поскольку res больше не управляет указателем (он был перемещен во временный объект), здесь ничего интересного не происходит.
- Временный объект перемещается присваиванием в mainres . Это переносит динамически созданный объект, хранящийся во временном объекте, в mainres .
- Выражение присваивания завершается, временный объект выходит за пределы области действия выражения и уничтожается. Однако, поскольку временный объект больше не управляет указателем (он был перемещен в mainres ), здесь также не происходит ничего интересного.
- В конце main() переменная mainres выходит из области видимости, и отображается наше последнее сообщение " Resource destroyed ".
Поэтому вместо того, чтобы копировать наш объект Resource дважды (один раз для конструктора копирования и один раз для присваивания копированием), мы дважды перемещаем его. Это более эффективно, поскольку объект Resource создается и уничтожается только один раз, а не три раза.
Когда вызываются конструктор перемещения и присваивание перемещением?
Конструктор перемещения и присваивание перемещением вызываются, когда эти функции определены, а аргументом для построения или присваивания является r-значение. Чаще всего это r-значение будет литералом или временным значением.
В большинстве случаев конструктор перемещения и оператор присваивания перемещением не предоставляются по умолчанию, если в классе нет определенных конструкторов копирования, присваивания копированием, присваивания перемещением или деструкторов. Однако дефолтные конструктор перемещения и присваивание перемещением делают то же самое, что и дефолтные конструктор копирования и присваивание копированием (делать копии, а не перемещают).
Правило
Если вам нужен конструктор перемещения и присваивание перемещением, выполняющее перемещения, вам нужно будет написать их самостоятельно.
Ключевой момент в семантике перемещения
Теперь у вас достаточно контекста для понимания ключевой идеи семантики перемещения.
Если мы создаем объект или выполняем присваивание, в котором аргументом является l-значение, единственное разумное, что мы можем сделать, – это скопировать l-значение. Мы не можем предположить, что изменение l-значения безопасно, потому что позже в программе оно может быть снова использовано. Если у нас есть выражение a = b , мы не можем ожидать каких-либо изменений b .
Однако, если мы создаем объект или выполняем присваивание, в котором аргументом является r-значение, тогда мы знаем, что r-значение – это всего лишь временный объект какого-то типа. Вместо того, чтобы копировать его (что может быть дорогостоящим), мы можем просто передать его ресурсы (что дешево) объекту, который мы создаем или которому выполняем присваивание. Это безопасно, потому что временный объект в любом случае будет уничтожен в конце выражения, поэтому мы знаем, что он больше никогда не будет использоваться!
C++11, через rvalue-ссылки, дает нам возможность обеспечивать различное поведение, когда аргументом является r-значение или l-значение, что позволяет нам принимать более разумные и эффективные решения о том, как должны вести себя наши объекты.
Функции перемещения должны всегда оставлять оба объекта в четко определенном состоянии.
В приведенных выше примерах и конструктор перемещения, и функции присваивания перемещением устанавливают a.m_ptr в значение nullptr . Это может показаться лишним – в конце концов, если a – временное r-значение, зачем беспокоиться о выполнении «очистки», если параметр a всё равно будет уничтожен?
Ответ прост: когда a выходит за пределы области видимости, вызывается деструктор a , и a.m_ptr удаляется. Если в этот момент a.m_ptr всё еще указывает на тот же объект, что и m_ptr , тогда m_ptr останется висячим указателем. Когда объект, содержащий m_ptr , в конечном итоге будет использован (или уничтожен), мы получим неопределенное поведение.
Кроме того, в следующем уроке мы увидим случаи, когда a может быть l-значением. В таком случае a не будет уничтожен немедленно, и его можно будет запросить еще до того, как истечет время его жизни.
Автоматические l-значения, возвращаемые по значению, могут быть перемещены вместо копирования
В функции generateResource() в примере выше с Auto_ptr4 , когда переменная res возвращается по значению, она перемещается, а не копируется, даже если res является l-значением. В спецификации C++ есть специальное правило, согласно которому автоматические объекты, возвращаемые функцией по значению, можно перемещать, даже если они являются l-значениями. Это имеет смысл, так как res всё равно будет уничтожен в конце функции! С таким же успехом мы могли бы забрать его ресурсы, вместо того, чтобы выполнять дорогостоящее и ненужное копирование.
Хотя компилятор может перемещать возвращаемые l-значения, в некоторых случаях он может добиться еще большего, просто полностью исключив копирование (что позволяет вовсе избежать необходимости выполнять копирование или перемещение). В таком случае не будут вызываться ни конструктор копирования, ни конструктор перемещения.
Отключение копирования
В приведенном выше классе Auto_ptr 4 мы оставили для сравнения конструктор копирования и оператор присваивания. Но в классах с поддержкой перемещения иногда желательно удалить функции конструктора копирования и присваивания копированием, чтобы гарантировать, что копии не будут созданы. В случае с нашим классом Auto_ptr мы не хотим копировать наш шаблонный объект T – потому что это дорого, и класс T может даже не поддерживать копирование!
Вот версия Auto_ptr , которая поддерживает семантику перемещения, но не поддерживает семантику копирования:
Если бы вы попытались передать функции l-значение Auto_ptr5 по значению, компилятор пожаловался бы, что конструктор копирования, необходимый для инициализации аргумента функции, был удален. Это хорошо, потому что мы, вероятно, всё равно должны передавать Auto_ptr5 по константной lvalue-ссылке!
Auto_ptr5 – это (наконец) хороший класс умных указателей. И на самом деле стандартная библиотека содержит класс, очень похожий на этот (и который вы должны использовать вместо этого), с именем std::unique_ptr . Подробнее об std::unique_ptr мы поговорим в этой главе позже.
Еще один пример
Давайте посмотрим на другой класс, который использует динамическую память: простой динамический шаблонный массив. Этот класс содержит конструктор копирования и оператор присваивания копированием, выполняющие глубокое копирование.
Теперь давайте, используем этот класс в программе, чтобы показать, как работает этот класс, когда мы размещаем миллион целых чисел в куче. Мы собираемся использовать класс Timer , который мы разработали в уроке «12.18 – Определение времени выполнения кода». Мы будем использовать его, чтобы измерить скорость выполнения нашего кода и показать вам разницу в производительности между копированием и перемещением.
На одной из машин автора в режиме релиза эта программа выполнилась за 0,00825559 секунды.
Теперь давайте снова запустим эту же программу, заменив конструктор копирования и присваивание копированием конструктором перемещения и присваиванием перемещением.
На той же машине эта программа была выполнена за 0,0056 секунды.
Сравним время выполнения этих двух программ, 0,0056 / 0,00825559 = 67,8%. Версия с перемещением была почти на 33% быстрее!
Конструктор перемещения
Вместо создания копии объекта можно переместить ресурсы из одного объекта в другой. Выполнить перемещение позволяет конструктор перемещения, который вызывается при инициализации временным объектом (значением r-value ), например, временным объектом, возвращаемым из функции. При возврате объекта из функции конструктор перемещения может не вызываться из-за оптимизаций вариантов RVO и NRVO компилятором (см. разд. 13.6). Чтобы вызвать конструктор перемещения явным образом можно воспользоваться функцией move() , которая выполняет приведение значения l-value к r-value :
Конструктор перемещения имеет следующий формат:
Название конструктора перемещения совпадает с названием класса. В качестве параметра конструктор принимает ссылку на значение r-value . Тип возвращаемого значения не указывается. Продемонстрируем использование конструкторов копирования и перемещения на примере (листинг 13.10). Дополнительно выполним перегрузку оператора присваивания для операций копирования и перемещения.
Листинг 13.10. Конструкторы копирования и перемещения
В этом примере внутри класса мы работаем с динамической памятью, поэтому необходимо явным образом реализовать как минимум конструктор копирования и оператор присваивания копированием, а также деструктор, внутри которого освобождается динамическая память. Если конструктор и метод не реализованы, то компилятор создает их реализацию автоматически, при этом выполняется поверхностное копирование объекта. Это означает, что будет скопирован адрес динамической памяти. В результате два объекта будут ссылаться на тот же самый участок памяти. Внутри деструктора мы вызываем оператор delete и освобождаем динамическую память. При вызове деструктора первого объекта все будет нормально, а вот вызов деструктора второго объекта приведет к попытке освободить уже не существующий участок памяти, что является ошибкой. Поэтому внутри конструктора копирования и метода присваивания копированием мы создаем полную копию объекта, выделяя динамическую память и копируя значения в нее с помощью функции memcpy() . Таким образом, объекты будут работать с разными участками памяти.
Внутри конструктора перемещения выполняется копирование адреса участка в динамической памяти. Второй объект является временным, поэтому достаточно выполнить поверхностную копию, но при этом мы присваиваем указателю внутри временного объекта нулевой указатель. Если это не сделать, то при удалении временного объекта будет вызван деструктор, внутри которого динамическая память освобождается. В результате внутри первого объекта будут выполняться операции с уже не существующим участком памяти, что является ошибкой. Поэтому не забудьте, во-первых, обнулить указатель внутри временного объекта, а, во-вторых, внутри метода присваивания перемещением освободить ранее выделенную динамическую память, чтобы избежать утечек памяти.
Итак, компилятор по умолчанию создает внутри класса следующие методы:
- конструктор по умолчанию. Если вы создали конструктор с параметрами, то конструктор по умолчанию автоматически не создается;
- конструктор копирования. Реализация по умолчанию выполняет поверхностное копирование объекта. Если выполняется работа с динамической памятью, то обязательно следует реализовать конструктор копирования явным образом;
- «операторный» метод присваивания копированием. Реализация по умолчанию выполняет поверхностное копирование объекта. Если выполняется работа с динамической памятью, то обязательно следует реализовать метод явным образом;
- конструктор перемещения. Если явным образом реализован конструктор копирования или метод присваивания копированием, то конструктор перемещения автоматически не создается. Если конструктора перемещения нет, то вызывается конструктор копирования;
- «операторный» метод присваивания перемещением. Если явным образом реализован конструктор копирования или метод присваивания копированием, то метод присваивания перемещением автоматически не создается. Если метода присваивания перемещением нет, то вызывается метод присваивания копированием;
- деструктор.
Все эти методы лучше реализовывать внутри класса явным образом или вместо реализации указать спецификаторы default или delete (см. разд. 13.8).
Учебник C++ (Qt Creator и MinGW) в формате PDF
Помощь сайту
ПАО Сбербанк:
Счет: 40817810855006152256
Реквизиты банка:
Наименование: СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК
Корреспондентский счет: 30101810500000000653
БИК: 044030653
КПП: 784243001
ОКПО: 09171401
ОКОНХ: 96130
Скриншот реквизитов
Конструктор переноса и оператор переноса. Назначение. Примеры. Ключевое слово noexcept . Пример класса Vector (динамический массив)
Перед изучением данной темы рекомендуется ознакомиться со следующей темой:
Содержание
- 1. Какие специальные функции класса предоставляются компилятором по умолчанию? Пример
- 2. Конструктор переноса и оператор переноса. Назначение. Особенности использования. Общая форма
- 3. Рисунок, демонстрирующий назначение конструктора переноса в классе
- 4. Случаи вызова конструктора переноса. Ссылки типа rvalue- и lvalue-. Действия, выполняемые в конструкторе переноса
- 5. Оператор переноса. Общая форма
- 6. Пример реализации класса Vector (динамический массив). Базовый набор методов. Конструктор переноса и оператор переноса в классе
- 6.1. Условие задачи
- 6.2. Решение
- 6.2.1. Составляющие класса Vector
- 6.2.2. Ввод внутренних полей класса
- 6.2.3. Внутренние private-функции Free() , CopyArray()
- 6.2.4. Конструктор с двумя параметрами Vector(double*, int)
- 6.2.5. Деструктор
Поиск на других ресурсах:
1. Какие специальные функции класса предоставляются компилятором по умолчанию? Пример
При начальном объявлении класса компилятор по умолчанию предоставляет 6 специальных функций, которые могут быть переопределены:
- конструктор по умолчанию (default constructor) или конструктор без параметров. Используется для создания экземпляра класса, если в классе не реализованы другие конструкторы;
- деструктор;
- конструктор копирования. Использует побитовое копирование данных экземпляров. Этот конструктор обязательно нужно переопределять в классах, где память для данных выделяется динамически. Если память в классах не выделяется динамически (с помощью указателей *), то этот конструктор переопределять не обязательно;
- оператор копирования. Использует побитовое копирование данных экземпляров. Этот оператор обязательно нужно переопределять в классах, где происходит динамическое выделение памяти под данные. Если в классе нет динамического выделения памяти, то оператро копирования можно и не переопределять;
- конструктор переноса (перемещения). Этот конструктор рекомендуется переопределять в классах, которые могут содержать (или содержат) большие массивы данных память для которых выделена динамически;
- оператор переноса (перемещения). Имеет такое же предназначение как конструктор переноса.
Любая из вышеперечисленных функций может быть переопределена в классе собственной реализацией.
Пример. Пусть задан пустой класс (заглушка) с именем MyClass
Для класса MyClass компилятор формирует 6 специальных функций по умолчанию. Сигнатура этих функций следующая:
-
MyClass() – конструктор по умолчанию. У этого конструктора нет параметров. Он актуален когда в классе нет ни одного конструктора, инициализирующего внутренние поля (данные) класса;
2. Конструктор переноса и оператор переноса. Назначение. Особенности использования. Общая форма
Конструктор переноса и оператор переноса был добавлен в C++ 11. Основная идея применения этих двух конструкций состоит в том, чтобы ускорить выполнение программы путем избежания копирования данных при начальной инициализации и присвоении так называемых rvalue-ссылок.
Конструктор переноса и оператор переноса целесообразно объявлять в классах, содержащих большие массивы данных. Никто не мешает объявить конструктор или оператор переноса в классе, в котором только несколько простых полей малой размерности. Однако в этом случае эффект от использования конструктора переноса будет незначительным (или вообще не будет).
Также если возникает необходимость использования этих конструкций, то рекомендуется их добавлять в паре (обе конструкции).
Если в классе не реализован конструктор переноса, то его вызов заменяется конструктором копирования. Если в классе не реализован оператор переноса, то его вызов заменяется оператором копирования.
Общая форма объявления конструктора переноса в классе
- ClassName – имя класса и конструктора;
- rObj – ссылка на ссылку на временный экземпляр класса (rvalue — экземпляр), значение которого будет скопировано в текущий экземпляр.
В приведенной выше общей форме используется ключевое слово noexcept . Этот спецификатор указывает, что наша функция (конструктор переноса) не генерирует (не выбрасывает) исключение или аварийно завершает свою работу. Компилятор рекомендует использовать слово noexcept для конструктора переноса и оператора переноса. В конструкторе переноса не происходит никаких операций с памятью (выделение памяти, освобождение памяти, запись данных в выделенную область памяти и т.п.), а происходит простое присвоение указателя (указателей).
3. Рисунок, демонстрирующий назначение конструктора переноса в классе
На рисунке 1 изображен случай, когда в классе не реализован конструктор переноса и вместо него вызывается конструктор копирования. Рассматривается копирование большого массива данных A размерностью n элементов для класса с именем MyClass . Как видно из рисунка, копируется весь массив поэлементно. Если количество элементов в этом массиве велико, этот процесс займет некоторое время.

Рисунок 1. Действия, выполняемые конструктором копирования. Копирование всего массива
После добавления конструктора переноса в класс MyClass , данные уже не копируются (рисунок 2). Это дает существенный выигрыш в скорости выполнения программы, если количество элементов в массиве obj.A значительно (например, n = 1E6).

Рисунок 2. Конструктор переноса. Присваивание (перенаправление) указателя на массив-источник. Данные не копируются
4. Случаи вызова конструктора переноса. Ссылки типа rvalue- и lvalue-. Действия, выполняемые в конструкторе переноса
В данной теме рассматриваются только общие особенности ссылок типа rvalue- и lvalue-. Подробный обзор lvalue- и rvalue, это уже совсем другая тема, требующая отдельного основательного исследования.
Если в классе объявлен конструктор переноса, то он вызывается в случаях, когда выражение, которое инициализирует значение экземпляра этого класса оператором = , получает другой экземпляр который есть так называемой rvalue- ссылкой.
Ниже рассматривается одна из возможных ситуаций вызова конструктора переноса.
В наиболее общем случае, любая функция, возвращающая экземпляр класса ClassName имеет вид:
Вызов конструктора переноса происходит при вызове функции SomeFunc() в момент объявления экземпляра класса
Экземпляр obj размещается в левой части оператора присваивания и есть ссылкой типа lvalue. Такая ссылка имеет область определения в пределах фигурных скобок и доступна после завершения текущего выражения. Иными словами, можно использовать экземпляр obj в дальнейшем, например
здесь SomeInternalMethod() – некоторый public -метод из класса ClassName .
В свою очередь, функция SumeFunc() размещается в правой части оператора = . Возвращаемый функцией результат есть экземпляром класса ClassName . В данном случае, этот экземпляр является временным объектом, относящимся к ссылке типа rvalue. Область действия данного временного объекта определяется текущим выражением. Этот временный экземпляр после выполнения присваивания уже не будет использоваться (рисунок 3).

Рисунок 3. Случай, когда вызывается конструктор переноса. Области действия lvalue- и rvalue- ссылок
Если в классе реализован собственный конструктор переноса, при подобных инициализациях этот конструктор будет вызываться вместо конструктора копирования. Если в конструкторе переноса выполнить правильное присваивание из rvalue- на lvalue-ссылки, то можно избежать лишнего копирования данных из области памяти, на которую указывает rvalue-ссылка, в область на которую указывает lvalue-ссылка. С увеличением объемов данных в классе (например, больших массивов информации) эффект от использования конструктора переноса будет возрастать.
В конструкторе переноса выполняются следующие действия:
- внутренние указатели должны быть перенаправлены на внешние данные, которые должны быть скопированы во внутренние поля класса. Иными словами, внутренние указатели получают значения адресов областей памяти, содержащих данные получаемые извне (смотрите пример ниже).
5. Оператор переноса. Общая форма
Цель использования оператора переноса такая же, как и конструктора переноса – ускорить выполнение программы за счет избежания непосредственного копирования данных при присваивании так называемых rvalue-ссылок, которые используются в выражениях в правой части оператора присваивания.
Если в классе объявлен оператор переноса, то он вызывается в случаях, когда в операторе присваивания ( = ) получается экземпляр класса, являющийся результатом возврата из другой функции
здесь SomeFunc() – некоторая функция, которая возвращает экземпляр класса ClassName .
Если в классе не реализован оператор переноса, то этот оператор заменяется оператором копирования.
Общая форма объявления оператора переноса в классе:
- ClassName – имя класса;
- obj – объект, который есть ссылкой типа rvalue в вызывающем выражении.
В операторе переноса последовательность выполняемых действий больше, чем в конструкторе копирования, а именно:
- проверка, не происходит ли присваивание экземпляра самому себе в случаях, когда функция может каким-либо образом возвращать этот же экземпляр (см. примеры ниже);
- освобождение памяти под выделенные внутренние данные. Экземпляр lvalue уже создан ранее и в нем уже есть некоторые данные;
- присваивание внутренним указателям адресов данных, которые необходимо скопировать в текущий экземпляр.
Более подробно реализацию оператора переноса смотрите в приведенном ниже примере.
6. Пример реализации класса Vector (динамический массив). Базовый набор методов. Конструктор переноса и оператор переноса в классе
В примере демонстрируется объявление и использование класса Vector , реализующего динамический массив типа double* . В классе, с целью демонстрации, используется базовый набор специальных функций класса и методов. Эти функции обеспечивают правильное функционирование экземпляров класса (выделение памяти, освобождение, использование исключительных ситуаций и т.п.). По желанию, можно перепрограммировать этот класс на одномерный массив для обобщенного типа T .
Также можно расширить класс путем добавления новых методов, оперирующих массивом. Например, можно добавить методы реверсирования массива, конкатенации, доступа к отдельным элементам массива по индексу и т.д.
6.1. Условие задачи
Разработать класс, являющийся динамическим массивом. В классе сформировать минимальный набор специальных функций для организации работы с массивом.
6.2. Решение
6.2.1. Составляющие класса Vector
Класс содержит следующие составляющие:
- A – массив типа double* ;
- count – количество элементов массива;
- Vector(double*, int) – параметризированный конструктор, инициализирующий данные класса;
- Vector() – конструктор без параметров;
- Vector(const Vector&) – конструктор копирования;
- Vector(Vector&&) – конструктор переноса;
- operator=(const Vector&) – оператор копирования;
- operator=(Vector&&) – оператор переноса;
Приведенный выше список является базовым (минимальным) набором функций, обеспечивающих правильное функционирование класса. По желанию, этот набор можно расширить дополнительными функциями.
6.2.2. Ввод внутренних полей класса
Динамический массив элементов типа double объявляется как double* . Количество элементов в массиве равно count . После ввода этих переменных класс имеет следующий вид
В нашем случае принимается договоренность, что проверку на наличие пустого массива осуществляем на основании значения count . Если count>0 , то массив не пуст. Во всех остальных случаях массив считается пустым. Контроль за заполнением массива полностью лежит на переменной count .
6.2.3. Внутренние private-функции Free() , CopyArray()
В разных методах класса программный код будет повторяться. Базовыми операциями, часто используемыми с массивом, являются:
- освобождение памяти, выделенной под массив;
- копирование внешнего массива во внутренний массив A.
Поэтому в классе целесообразно реализовать соответствующие внутренние private-функции Free() и CopyArray() . После ввода функций, программный код класса следующий:
Функция Free() освобождает память, выделенную под массив A . Эта функция будет вызываться из других функций в случаях, когда требуется выполнить перераспределение памяти или освобождение памяти.
В функции Free() факт наличия частей в массиве проверяется по переменной count ( count==0 ). Поэтому, в случае пустого массива, не нужно каждый раз присваивать указателю A значение nullptr .
6.2.4. Конструктор с двумя параметрами Vector(double*, int)
При проектировании классов для инициализации внутренних данных может использоваться разное количество конструкторов. Первым вводится конструктор, инициализирующий массив с охватом наибольшего количества внутренних полей класса. В нашем случае в раздел public вводится следующий конструктор с двумя параметрами:
В конструкторе используется внутренняя функция CopyArray() для копирования данных во внутренний массив A .
6.2.5. Деструктор
После объявления параметризированного конструктора, обязательно объявляется деструктор, вызывающий внутреннюю функцию Free() .
6.2.6. Конструктор без параметров Vector()
Еще один конструктор, который может использоваться для создания пустого массива – конструктор без параметров. Данный конструктор делегирует свои полномочия конструктору с двумя параметрами. Конструктор вводится в раздел public.
Это единственный случай в программе, когда используется nullptr для присваивания значения указателю A . Во всех остальных случаях не нужно присваивать указателю A значение nullptr , поскольку контроль за фактом наличия пустого массива полностью возлагается на переменную count .
6.2.7. Конструктор копирования Vector(const Vector&)
Поскольку в нашем классе используется динамическое выделение памяти под внутренние данные, обязательно нужно использовать конструктор копирования во избежание недостатков побитового копирования. Более подробно об этом описывается здесь и здесь .
В нашем случае, код конструктора копирования чрезвычайно прост. Вызывается функция CopyArray() , выполняющая всю необходимую работу.
6.2.8. Оператор копирования operator=(const Vector&)
Оператор копирования необходимо реализовывать в тех случаях, когда в классе используется динамическое выделение памяти во внутренние поля. В нашем случае, код оператора копирования содержит вызов функции CopyArray() .
6.2.9. Конструктор переноса Vector(Vector&&)
Программный код конструктора переноса не выполняет никаких операций с памятью (выделение памяти, освобождение и т.д.).
В конструкторе переноса интерес представляет строка
Такое действие является обязательным, поскольку при присваивании указателей
у нас получается ситуация, что оба указателя ( A и obj.A ) указывают на один и тот же участок памяти. В случае освобождения памяти под указатели, один и тот же участок памяти будет освобождаться дважды, а это приведет к генерированию исключительной ситуации. Чтобы избежать этого, количество элементов во временном объекте obj.count устанавливается равным 0. При вызове функции Free() , освобождающей память, происходит проверка count на ненулевое значение, если count==0 , то память не освобождается, а, следовательно, лишнего (ненужного) освобождения памяти не произойдет.
6.2.10. Оператор переноса operator=(Vector&&)
Поскольку в нашем классе есть динамический массив, который может иметь произвольное количество элементов, то рекомендуется объявить в нем оператор переноса.
6.2.11. Метод Set(double*, int) . Установить новый массив
С целью демонстрации, в классе реализован метод Set() , производящий копию внешнего массива, являющегося входным параметром, во внутренний массив.
Ошибочным будет присвоение вроде
поскольку два массива (внешний и внутренний) будут указывать на один участок памяти, что в некоторых ситуациях может привести к трудноуловимым ошибкам.
6.2.12. Метод Print(string) . Вывести массив с заданным сообщением
В целях получения текущего состояния класса, вводится метод Print() .
6.2.13. Общая структура класса Vector
После выполнения пунктов 4.2.2 – 4.2.13 класс Vector в сокращенном виде будет следующим.
6.2.14. Метод GetV() . Получить произвольный массив
С целью демонстрации вызова конструктора переноса и оператора переноса вне класса Vector вводится функция GetV() , которая формирует произвольный массив и возвращает экземпляр типа Vector .
6.2.15. Функция main()
Тест работы класса Vector происходит в функции main() .
6.2.16. Общая структура программы
В наиболее общем случае структура программы имеет вид
6.2.17. Запуск программы
После объединения всех вышеприведенных фрагментов кода запуск программы выдаст следующий результат
Move-семантика в C++
Move-семантика даёт возможность перемещать объект, а не копировать его, что увеличивает производительность. Давайте посмотрим, как работает семантика на примере, в котором будем использовать класс String:

Обратите внимание, что во время передачи объекта этого класса в функцию, которая принимает его по значению (давайте назовём её by_value() ) произойдёт вот что:

То есть мы видим четыре обращения к аллокатору, а это довольно накладно. Однако если объект String нам более не понадобится, при этом функцию by_value() изменять нельзя, мы можем переместить объект, не копируя его. Чтобы это осуществить, надо написать для класса String конструктор перемещения:

Итак, во-первых, параметр конструктора перемещения other является неконстантным, так как конструктор его меняет. Во-вторых, параметр является не lvalue-ссылкой (&), а rvalue-ссылкой (&&). При этом сам конструктор переносит Си-строку с other на this, то есть делает other пустым.
В общем случае конструктор перемещения не медленнее, а часто быстрее конструктора копирования, однако ничего не мешает разработчику поместить в конструктор перемещения sleep(10’000).
Чтобы вызвать конструктор перемещения, а не конструктор копирования, вы можете задействовать std::move() . В результате наш пример станет выглядеть так:
Итог использования move-семантики следующий: число обращений к аллокатору уменьшилось в два раза!