Копирующий и перемещающий конструктор

Обновлено: 16.04.2024

Рассмотрим функцию, в которой мы динамически размещаем значение:

Хотя приведенный выше код кажется довольно простым, довольно легко забыть освободить ptr . Даже если вы не забыли удалить ptr в конце функции, существует множество способов, которыми ptr может быть не удален, если функция завершится раньше времени. Это может произойти за счет преждевременного возврата:

или через выброшенное исключение:

В двух показанных выше программах выполняется преждевременная инструкция return или throw , в результате чего функция завершается без удаления переменной ptr . Следовательно, происходит утечка памяти, выделенной для переменной ptr (и эта утечка будет происходить снова каждый раз, когда эта функция вызывается и возвращается, не доходя до конца).

По сути, проблемы такого рода возникают из-за того, что переменные-указатели не имеют встроенного механизма для очистки после себя.

На помощь приходят классы умных указателей?

Одна из лучших особенностей классов заключается в том, что они содержат деструкторы, которые автоматически запускаются, когда объект класса выходит за пределы области видимости. Поэтому, если вы выделяете (или приобретаете) память в конструкторе, вы можете освободить ее в деструкторе и гарантировать, что память будет освобождена, когда объект класса будет уничтожен (независимо от того, выходит ли он за пределы области видимости, удаляется явно, и так далее…). Это лежит в основе парадигмы программирования RAII, о которой мы говорили в уроке «12.9 – Деструкторы».

Итак, можем ли мы использовать класс для помощи в управлении указателями и их очистки? Можем!

Рассмотрим класс, единственной задачей которого было удерживать и «владеть» переданным ему указателем, а затем освобождать память, на которую указывает этот указатель, когда объект класса выходит за пределы области видимости. Пока объекты этого класса создаются только как локальные переменные, мы можем гарантировать, что класс правильно выйдет за пределы области видимости (независимо от того, когда и как завершаются наши функции), и принадлежащий ему указатель будет уничтожен.

Вот первый набросок идеи:

Эта программа печатает:

Рассмотрим, как работают эти программа и класс. Сначала мы динамически создаем Resource и передаем его в качестве параметра нашему шаблонному классу Auto_ptr1 . С этого момента наша переменная Auto_ptr1 res владеет этим объектом Resource ( Auto_ptr1 имеет композиционную связь с m_ptr ). Поскольку res объявлена как локальная переменная и имеет область видимости блока, она выйдет из области видимости, когда этот блок закончится, и будет уничтожена (не беспокойтесь о том, чтобы не забыть освободить ее память). И поскольку это класс, при уничтожении его объекта будет вызван деструктор Auto_ptr1 . Этот деструктор гарантирует, что указатель Resource , который хранится объектом, будет удален!

Пока Auto_ptr1 определен как локальная переменная (с автоматической продолжительностью жизни, отсюда часть имени класса " Auto "), Resource будет гарантированно уничтожен в конце блока, в котором он объявлен, независимо от того, как функция завершается (даже если она завершается преждевременно).

Такой класс называется умным указателем. Умный указатель – это класс композиции, который предназначен для управления динамически выделяемой памятью и гарантирования освобождения этой памяти, когда объект умного указателя выходит за пределы области видимости. (Соответственно, встроенные указатели иногда называют «тупыми указателями», потому что они не могут выполнить после себя очистку.)

Теперь вернемся к нашему примеру someFunction() выше и покажем, как класс умного указателя может решить нашу проблему:

Если пользователь вводит ненулевое целое число, показанная выше программа напечатает:

Если пользователь вводит ноль, эта программа преждевременно завершает работу и печатает:

Обратите внимание, что даже в том случае, когда пользователь вводит ноль и функция преждевременно завершается, объект Resource всё равно удаляется корректно.

Поскольку переменная ptr является локальной переменной, он будет уничтожен при завершении функции (независимо от того, как она завершится). И поскольку деструктор Auto_ptr1 удаляет Resource , мы уверены, что Resource будет удален правильным образом.

Критический недостаток

Класс Auto_ptr1 имеет критический недостаток, скрывающийся за автоматически сгенерированным кодом. Прежде чем читать дальше, посмотрите, сможете ли вы определить, в чем заключается этот недостаток. Подождем.

Подсказка: подумайте, какие части класса будут автоматически сгенерированы, если вы их не предоставите.

Хорошо, время вышло.

Вместо разговоров, мы вам покажем его. Рассмотрим следующую программу:

Эта программа печатает:

Очень вероятно (но не обязательно) ваша программа на этом этапе завершится со сбоем. Теперь видите проблему? Поскольку мы не предоставили конструктор копирования или оператор присваивания, C++ сам предоставляет их нам. А функции, которые он предоставляет, выполняют поверхностное копирование. Поэтому, когда мы инициализируем res2 с помощью res1 , обе переменные Auto_ptr1 указывают на один и тот же объект Resource . Когда res2 выходит за пределы области видимости, она удаляет этот объект Resource , оставляя res1 с висячим указателем. Когда res1 удаляет свой (уже удаленный) объект Resource , происходит сбой!

Вы столкнетесь с аналогичной проблемой и с подобной функцией:

В этой программе res1 будет скопирован по значению в параметр res функции passByValue , что приведет к дублированию указателя на объект Resource . И снова сбой!

Ясно, что это нехорошо. Как мы можем решить эту проблему?

Что ж, одна вещь, которую мы могли бы сделать, – это явно определить и удалить конструктор копирования и оператор присваивания, тем самым, в первую очередь, предотвращая создание любых копий. Это предотвратит передачу по значению (что хорошо, мы, вероятно, всё равно не должны передавать эти объекты по значению).

Но как тогда вернуть Auto_ptr1 из функции обратно вызывающей функции?

Мы не можем вернуть наш Auto_ptr1 по ссылке, потому что локальный Auto_ptr1 в конце функции будет уничтожен, и вызывающая функция останется с висячей ссылкой. Возврат по адресу имеет ту же проблему. Мы могли бы вернуть указатель r по адресу, но потом, позже, мы могли бы забыть удалить r , в чем, в первую очередь, и заключается весь смысл использования умных указателей. Так что это исключено. Возвращение Auto_ptr1 по значению – единственный вариант, который имеет смысл, но тогда мы получаем поверхностное копирование, дублированные указатели и сбои.

Другой вариант – переопределить конструктор копирования и оператор присваивания для создания глубоких копий. Таким образом, мы, по крайней мере, гарантированно избежим дублирования указателей на один и тот же объект. Но копирование может быть дорогостоящим (и может быть нежелательным или даже невозможным), и мы не хотим делать ненужные копии объектов только для того, чтобы вернуть Auto_ptr1 из функции. Кроме того, при присваивании или инициализации «глупого» указателя объект, на который он указывает, не копируется, так почему же нам ожидать, что умные указатели будут вести себя иначе?

Семантика перемещения

Что, если вместо того, чтобы заставить наш конструктор копирования и оператор присваивания копировать указатель («семантика копирования»), мы вместо этого передадим/переместим владение указателем от источника к объекту назначения? Это основная идея семантики перемещения. Семантика перемещения означает, что класс передаст право собственности на объект, а не создаст копию.

Давайте обновим наш класс Auto_ptr1 , чтобы показать, как это можно сделать:

Эта программа печатает:

Обратите внимание, что наш перегруженный operator= передал право владения m_ptr от res1 к res2 ! Следовательно, мы не получаем дубликатов указателя, и всё аккуратно очищается.

std::auto_ptr и почему это была плохая идея

Теперь самое время поговорить об std::auto_ptr . std::auto_ptr , представленный в C++98 и удаленный в C++17, был первой попыткой C++ создать стандартизированный умный указатель. std::auto_ptr решил реализовать семантику перемещения так, как это делает класс Auto_ptr2 .

Однако std::auto_ptr (и наш класс Auto_ptr2 ) имеет ряд проблем, делающих его использование опасным.

Во-первых, поскольку std::auto_ptr реализует семантику перемещения через конструктор копирования и оператор присваивания, передача std::auto_ptr в функции по значению приведет к тому, что ваш ресурс будет перемещен в параметр функции (и будет уничтожен в конце функции, когда параметры функции выходят из области видимости). Затем, когда вы возвращаетесь к аргументу auto_ptr в вызывающей функции (не осознавая, что его ресурс был передан и удален), вы внезапно разыменовываете нулевой указатель. Сбой программы!

Во-вторых, std::auto_ptr всегда удаляет свое содержимое, не используя удаление для массива. Это означает, что auto_ptr будет работать неправильно с динамически размещаемыми массивами, потому что он использует неправильный способ освобождения памяти. Хуже того, это не помешает вам передать ему динамический массив, которым он затем будет неправильно управлять, что приведет к утечке памяти.

Наконец, auto_ptr плохо сочетается со многими другими классами стандартной библиотеки, включая большинство контейнеров и алгоритмов. Это происходит потому, что эти классы стандартной библиотеки предполагают, что при копировании элемента на самом деле создается копия, а не перемещение.

Из-за приведенных выше недостатков std::auto_ptr стал считаться устаревшим в C++11 и был удален в C++17.

Прогресс

Основная проблема с std::auto_ptr заключается в том, что до C++11 в языке C ++ просто не было механизма, позволяющего отличить «семантику копирования» от «семантики перемещения». Переопределение семантики копирования для реализации семантики перемещения приводит к странным случаям и непреднамеренным ошибкам. Например, вы можете написать res1 = res2 и не знать, изменится ли res2 или нет!

По этой причине в C++11 была официально определена концепция «перемещения», и в язык была добавлена «семантика перемещения», чтобы правильно отличать копирование от перемещения. Теперь, когда мы подготовили почву для понимания того, почему семантика перемещения может быть полезна, мы исследуем тему семантики перемещения в оставшейся части этой главы. Мы также исправим наш класс Auto_ptr2 , используя семантику перемещения.

В C++11 std::auto_ptr был заменен множеством других типов умных указателей, «учитывающих перемещение»: std::unique_ptr , std::weak_ptr и std::shared_ptr . Мы также рассмотрим два самых популярных из них: unique_ptr (который является прямой заменой auto_ptr ) и shared_ptr .

В этом разделе описывается, как написать конструктор перемещения и оператор присваивания перемещения для класса C++. Конструктор перемещения позволяет перемещать ресурсы, принадлежащие объекту rvalue, в lvalue без копирования. Дополнительные сведения о семантике перемещения см. в описании декларатора ссылки Rvalue: &&.

Этот раздел построен на основе приведенного ниже класса C++ MemoryBlock , который управляет буфером памяти.

В следующих процедурах описывается создание конструктора перемещения и оператора присваивания перемещения для этого примера класса C++.

Создание конструктора перемещения для класса C++

Определите пустой метод конструктора, принимающий в качестве параметра ссылку rvalue на тип класса, как показано в следующем примере:

В конструкторе перемещения присвойте создаваемому объекту данные-члены класса из исходного объекта:

Присвойте данным-членам исходного объекта значения по умолчанию. Это не позволяет деструктору многократно освобождать ресурсы (например, память):

Создание оператора присваивания перемещения для класса C++

Определите пустой оператор присваивания, принимающий в качестве параметра ссылку rvalue на тип класса и возвращающий ссылку на тип класса, как показано в следующем примере:

В операторе присваивания перемещения добавьте условный оператор, который не выполняет никакой операции при попытке присвоить объект самому себе.

В условном операторе освободите все ресурсы (такие как память) из объекта, которому производится присваивание.

В следующем примере освобождается член _data из объекта, которому производится присваивание:

Выполните шаги 2 и 3 из первой процедуры, чтобы переместить данные-члены из исходного объекта в создаваемый объект:

Верните ссылку на текущий объект, как показано в следующем примере:

Пример. Полный конструктор перемещения и оператор присваивания

В следующем примере показаны полные конструктор перемещения и оператор назначения перемещения для класса MemoryBlock :

Пример использования семантики перемещения для повышения производительности

В следующем примере показано, как семантика перемещения может повысить производительность приложений. В примере добавляются два элемента в объект-вектор, а затем вставляется новый элемент между двумя существующими элементами. Класс vector использует семантику перемещения для эффективного выполнения операции вставки, перемещая элементы вектора вместо копирования.

В этом примере выводятся следующие данные:

До Visual Studio 2010 г. в этом примере выводятся следующие выходные данные:

Версия этого примера, в которой используется семантика перемещения, более эффективна, чем версия, в которой эта семантика не используется, поскольку в ней выполняется меньше операций копирования, выделения памяти и освобождения памяти.

Отказоустойчивость

Во избежание утечки ресурсов (таких как память, дескрипторы файлов и сокеты) обязательно освобождайте их в операторе присваивания перемещения.

Чтобы предотвратить невосстановимое уничтожение ресурсов, в операторе присваивания перемещения необходимо правильно обрабатывать присваивания самому себе.

Если для класса определены как конструктор перемещения, так и оператор присваивания перемещения, можно исключить избыточный код, написав конструктор перемещения так, чтобы он вызывал оператор присваивания перемещения. В следующем примере показана измененная версия конструктора перемещения, вызывающая оператор присваивания перемещения:

Чтобы настроить, как класс инициализирует его члены или вызывать функции при создании объекта класса, определите конструктор. Конструкторы имеют имена, совпадающие с именами классов, и не имеют возвращаемых значений. Вы можете определить столько перегруженных конструкторов, сколько необходимо для настройки инициализации различными способами. Как правило, конструкторы имеют открытые специальные возможности, чтобы код за пределами определения класса или иерархии наследования может создавать объекты класса. Но вы также можете объявить конструктор как protected или private .

Конструкторы могут при необходимости принимать список инициализаторов элементов. Это более эффективный способ инициализации членов класса, чем назначение значений в тексте конструктора. В следующем примере показан класс Box с тремя перегруженными конструкторами. Последние два используют списки инициализации элементов:

При объявлении экземпляра класса компилятор выбирает, какой конструктор будет вызываться на основе правил разрешения перегрузки:

  • Конструкторы могут быть объявлены как inline , , explicitfriend или constexpr .
  • Конструктор может инициализировать объект, объявленный как const , volatile или const volatile . Объект становится const после завершения конструктора.
  • Чтобы определить конструктор в файле реализации, присвойте ему полное имя, как и любая другая функция-член: Box::Box() .

Списки инициализаторов элементов

При необходимости конструктор может иметь список инициализаторов элементов, который инициализирует члены класса перед запуском тела конструктора. (Список инициализаторов элементов не совпадает со списком инициализаторов типа std::initializer_list .)

Предпочитать инициализаторы элементов перечисляют значения вместо назначения значений в тексте конструктора. Список инициализаторов элементов напрямую инициализирует элементы. В следующем примере показан список инициализаторов элементов, состоящий из всех identifier(argument) выражений после двоеточия:

Идентификатор должен ссылаться на член класса; он инициализирован со значением аргумента. Аргумент может быть одним из параметров конструктора, вызова функции или . std::initializer_list

const члены и члены ссылочного типа должны быть инициализированы в списке инициализаторов элементов.

Чтобы обеспечить полную инициализацию базовых классов перед запуском производного конструктора, вызовите все параметризованные конструкторы базового класса в списке инициализаторов.

Конструкторы по умолчанию

Конструкторы по умолчанию обычно не имеют параметров, но они могут иметь параметры со значениями по умолчанию.

Конструкторы по умолчанию являются одной из специальных функций-членов. Если конструкторы в классе не объявляются, компилятор предоставляет неявный inline конструктор по умолчанию.

Если используется неявный конструктор по умолчанию, обязательно инициализировать элементы в определении класса, как показано в предыдущем примере. Без этих инициализаторов члены будут неинициализированы, а вызов Volume() создаст значение мусора. Как правило, рекомендуется инициализировать элементы таким образом, даже если не используется неявный конструктор по умолчанию.

Вы можете запретить компилятору создавать неявный конструктор по умолчанию, определив его как удаленный:

Конструктор по умолчанию, созданный компилятором, будет определен как удаленный, если какие-либо члены класса не являются конструктором по умолчанию. Например, все члены типа класса и их члены класса должны иметь конструктор по умолчанию и деструкторы, которые доступны. Все члены данных ссылочного типа и все const члены должны иметь инициализатор элементов по умолчанию.

При вызове конструктора по умолчанию, созданного компилятором, и пытаетесь использовать круглые скобки, выдается предупреждение:

Это утверждение является примером проблемы "Большинство vexing Parse". Можно интерпретировать myclass md(); как объявление функции или как вызов конструктора по умолчанию. Поскольку средства синтаксического анализа C++ предпочитают объявления по сравнению с другими вещами, выражение рассматривается как объявление функции. Дополнительные сведения см. в разделе "Большинство синтаксического анализа".

Если объявлены какие-либо конструкторы, отличные от по умолчанию, компилятор не предоставляет конструктор по умолчанию:

Если у класса нет конструктора по умолчанию, массив объектов этого класса нельзя создать с помощью синтаксиса квадратной скобки. Например, учитывая предыдущий блок кода, массив Boxes нельзя объявить следующим образом:

Однако для инициализации массива объектов Box можно использовать набор списков инициализаторов:

Дополнительные сведения см. в разделе "Инициализаторы".

Конструкторы копии

Конструктор копирования инициализирует объект, копируя значения элементов из объекта того же типа. Если члены класса являются простыми типами, такими как скалярные значения, конструктор копирования, созданный компилятором, достаточно, и вам не нужно определять собственные. Если для класса требуется более сложная инициализация, необходимо реализовать пользовательский конструктор копирования. Например, если член класса является указателем, необходимо определить конструктор копирования для выделения новой памяти и копирования значений из объекта, на который указывает другой объект. Конструктор копирования, созданный компилятором, просто копирует указатель, чтобы новый указатель по-прежнему указывал на расположение памяти другого пользователя.

Конструктор копирования может иметь одну из следующих сигнатур:

При определении конструктора копирования необходимо также определить оператор присваивания копирования (=). Дополнительные сведения см. в разделе "Назначение " и " Копирование конструкторов" и операторов присваивания копирования.

Вы можете запретить копирование объекта, определив конструктор копирования как удаленный:

При попытке копирования объекта возникает ошибка C2280: попытка ссылаться на удаленную функцию.

Конструкторы перемещения

Конструктор перемещения — это специальная функция-член, которая перемещает владение данными существующего объекта в новую переменную без копирования исходных данных. Он принимает ссылку rvalue в качестве первого параметра, а все последующие параметры должны иметь значения по умолчанию. Конструкторы перемещения могут значительно повысить эффективность программы при передаче больших объектов.

Компилятор выбирает конструктор перемещения, когда объект инициализируется другим объектом того же типа, если другой объект будет уничтожен и больше не нуждается в его ресурсах. В следующем примере показано одно дело, когда конструктор перемещения выбирается с помощью разрешения перегрузки. В конструкторе, который вызывает get_Box() , возвращаемое значение является xvalue (значение eXpiring). Поэтому он не назначается какой-либо переменной и поэтому выходит за пределы области действия. Чтобы обеспечить мотивацию для этого примера, давайте предоставим Box большой вектор строк, представляющих его содержимое. Вместо копирования вектора и его строк конструктор перемещения "крадет" его из значения "box", чтобы вектор теперь принадлежит новому объекту. Вызов std::move необходим, так как оба vector класса string реализуют собственные конструкторы перемещения.

Если класс не определяет конструктор перемещения, компилятор создает неявный конструктор, если конструктор копирования не объявлен пользователем, оператор назначения копирования, оператор перемещения или деструктор. Если не определен явный или неявный конструктор перемещения, операции, в противном случае использующие конструктор перемещения, используют конструктор копирования. Если класс объявляет конструктор перемещения или оператор присваивания перемещения, неявно объявленный конструктор копирования определяется как удаленный.

Неявно объявленный конструктор перемещения определяется как удаленный, если какие-либо элементы, являющиеся типами классов, не имеют деструктора или если компилятор не может определить, какой конструктор следует использовать для операции перемещения.

Дополнительные сведения о написании конструктора нетривиального перемещения см. в разделе "Конструкторы перемещения" и "Операторы присваивания перемещения" (C++).

Явно заданные по умолчанию и удаленные конструкторы

Конструкторы копирования по умолчанию , конструкторы по умолчанию, конструкторы перемещения, операторы присваивания копирования, операторы присваивания перемещения и деструкторы. Вы можете явно удалить все специальные функции-члены.

Конструкторы constexpr

Конструктор может быть объявлен как constexpr , если

  • он либо объявлен как стандартный, либо удовлетворяет всем условиям для функций constexpr в целом;
  • класс не имеет виртуальных базовых классов;
  • каждый из параметров является литеральным типом;
  • тело не является блоком try-block функции;
  • инициализированы все нестатические члены данных и подобъекты базового класса;
  • Значение , если класс является (a) объединением, имеющим члены варианта, или (б) имеет анонимные объединения, инициализируется только один из членов профсоюза;
  • каждый нестатический член данных типа класса, а все подобъекты базового класса имеют конструктор constexpr.

Конструкторы списков инициализаторов

Затем создайте объекты Box следующим образом:

Явные конструкторы

Если у класса имеется конструктор с одним параметром, или у всех параметров, кроме одного, имеются значения по умолчанию, тип параметра можно неявно преобразовать в тип класса. Например, если у класса Box имеется конструктор, подобный следующему:

Можно инициализировать Box следующим образом:

Или передать целое значение функции, принимающей объект Box:

В некоторых случаях подобные преобразования могут быть полезны, однако чаще всего они могут привести к незаметным, но серьезным ошибкам в вашем коде. Как правило, необходимо использовать ключевое explicit слово в конструкторе (и определяемых пользователем операторах), чтобы предотвратить такое неявное преобразование типов:

Когда конструктор является явным, эта строка вызывает ошибку компилятора: ShippingOrder so(42, 10.8); . Дополнительные сведения см. в разделе о преобразованиях определяемых пользователем типов.

Порядок строительства

Конструктор выполняет свою работу в следующем порядке.

Вызывает конструкторы базовых классов и членов в порядке объявления.

Если класс является производным от виртуальных базовых классов, конструктор инициализирует указатели виртуальных базовых классов объекта.

Если класс имеет или наследует виртуальные функции, конструктор инициализирует указатели виртуальных функций объекта. Указатели виртуальных функций указывают на таблицу виртуальных функций класса, чтобы обеспечить правильную привязку вызовов виртуальных функций к коду.

Выполняет весь код в теле функции.

В следующем примере показан порядок, в котором конструкторы базовых классов и членов вызываются в конструкторе для производного класса. Сначала вызывается базовый конструктор. Затем члены базового класса инициализируются в том порядке, в котором они отображаются в объявлении класса. Наконец, вызывается производный конструктор.

Выходные данные будут выглядеть следующим образом.

Конструктор производного класса всегда вызывает конструктор базового класса, чтобы перед выполнением любых дополнительных операций иметь в своем распоряжении полностью созданные базовые классы. Конструкторы базового класса вызываются в порядке наследования, например, если ClassA является производным от , производным от ClassC ClassB которого является конструктор, ClassC сначала вызывается конструктор, а затем ClassB конструктор, а затем ClassA конструктор.

Если базовый класс не имеет конструктора по умолчанию, необходимо указать параметры конструктора базового класса в конструкторе производного класса:

Если конструктор создает исключение, то удаление выполняется в порядке, обратном созданию.

Отменяется код в теле функции конструктора.

Объекты базовых классов и объекты-члены удаляются в порядке, обратном объявлению.

Если конструктор не делегируется, все полностью созданные объекты базового класса и члены уничтожаются. Однако поскольку сам объект не полностью построен, деструктор не выполняется.

Производные конструкторы и расширенная инициализация агрегатов

Если конструктор базового класса не является открытым, но доступен для производного класса, нельзя использовать пустые фигурные скобки для инициализации объекта производного типа в /std:c++17 режиме, а затем в Visual Studio 2017 и более поздних версий.

В следующем примере показана соответствующая реакция на событие в C++14:

В C++17 Derived теперь считается агрегатным типом. Это означает, что инициализация Base через закрытый конструктор по умолчанию происходит непосредственно как часть расширенного правила агрегатной инициализации. Ранее частный Base конструктор был вызван через Derived конструктор, и он был успешно выполнен из-за friend объявления.

В следующем примере показано поведение C++17 в Visual Studio 2017 и более поздних версий в /std:c++17 режиме:

Конструкторы для классов с множественным наследованием

Если класс является производным от нескольких базовых классов, конструкторы базового класса вызываются в порядке, в котором они перечислены в объявлении производного класса:

Должны выводиться следующие выходные данные:

Делегирующие конструкторы

Делегирующий конструктор вызывает другой конструктор в том же классе для выполнения некоторых действий по инициализации. Эта функция полезна, если у вас есть несколько конструкторов, которые все должны выполнять аналогичную работу. Основную логику можно написать в одном конструкторе и вызвать из других. В следующем тривиальном примере Box(int) делегирует свою работу Box(int,int,int):

Объект, созданный конструкторами, полностью инициализируется сразу после выполнения любого конструктора. Дополнительные сведения см. в разделе "Делегирование конструкторов".

Наследование конструкторов (C++11)

Производный класс может наследовать конструкторы от прямого базового класса с помощью using объявления, как показано в следующем примере:

Visual Studio 2017 и более поздних версий: оператор using в /std:c++17 режиме и более поздних версиях преобразует все конструкторы из базового класса, за исключением тех, которые имеют идентичную сигнатуру конструкторам в производном классе. Как правило, рекомендуется использовать наследуемые конструкторы, когда производный класс не объявляет новые члены данных или конструкторы.

Шаблон класса может наследовать все конструкторы от аргумента типа, если этот тип определяет базовый класс:

Производный класс не может наследоваться от нескольких базовых классов, если эти базовые классы имеют конструкторы с одинаковой сигнатурой.

Конструкторы и составные классы

Классы, содержащие члены типа класса, называются составными классами. При создании члена типа класса составного класса конструктор вызывается перед собственным конструктором класса. Если у содержащегося класса нет конструктора по умолчанию, необходимо использовать список инициализации в конструкторе составного класса. В предыдущем примере StorageBox при присвоении типу переменной-члена m_label нового класса Label необходимо вызвать конструктор базового класса и инициализировать переменную m_label в конструкторе StorageBox :

В этом разделе описывается, как написать конструктор перемещения и оператор присваивания перемещения для класса C++. Конструктор перемещения позволяет перемещать ресурсы, принадлежащие объекту rvalue, в lvalue без копирования. Дополнительные сведения о семантике перемещения см. в описании декларатора ссылки Rvalue: &&.

Этот раздел построен на основе приведенного ниже класса C++ MemoryBlock , который управляет буфером памяти.

В следующих процедурах описывается создание конструктора перемещения и оператора присваивания перемещения для этого примера класса C++.

Создание конструктора перемещения для класса C++

Определите пустой метод конструктора, принимающий в качестве параметра ссылку rvalue на тип класса, как показано в следующем примере:

В конструкторе перемещения присвойте создаваемому объекту данные-члены класса из исходного объекта:

Присвойте данным-членам исходного объекта значения по умолчанию. Это не позволяет деструктору многократно освобождать ресурсы (например, память):

Создание оператора присваивания перемещения для класса C++

Определите пустой оператор присваивания, принимающий в качестве параметра ссылку rvalue на тип класса и возвращающий ссылку на тип класса, как показано в следующем примере:

В операторе присваивания перемещения добавьте условный оператор, который не выполняет никакой операции при попытке присвоить объект самому себе.

В условном операторе освободите все ресурсы (такие как память) из объекта, которому производится присваивание.

В следующем примере освобождается член _data из объекта, которому производится присваивание:

Выполните шаги 2 и 3 из первой процедуры, чтобы переместить данные-члены из исходного объекта в создаваемый объект:

Верните ссылку на текущий объект, как показано в следующем примере:

Пример. Полный конструктор перемещения и оператор присваивания

В следующем примере показаны полные конструктор перемещения и оператор назначения перемещения для класса MemoryBlock :

Пример использования семантики перемещения для повышения производительности

В следующем примере показано, как семантика перемещения может повысить производительность приложений. В примере добавляются два элемента в объект-вектор, а затем вставляется новый элемент между двумя существующими элементами. Класс vector использует семантику перемещения для эффективного выполнения операции вставки, перемещая элементы вектора вместо копирования.

В этом примере выводятся следующие данные:

До Visual Studio 2010 г. в этом примере выводятся следующие выходные данные:

Версия этого примера, в которой используется семантика перемещения, более эффективна, чем версия, в которой эта семантика не используется, поскольку в ней выполняется меньше операций копирования, выделения памяти и освобождения памяти.

Отказоустойчивость

Во избежание утечки ресурсов (таких как память, дескрипторы файлов и сокеты) обязательно освобождайте их в операторе присваивания перемещения.

Чтобы предотвратить невосстановимое уничтожение ресурсов, в операторе присваивания перемещения необходимо правильно обрабатывать присваивания самому себе.

Если для класса определены как конструктор перемещения, так и оператор присваивания перемещения, можно исключить избыточный код, написав конструктор перемещения так, чтобы он вызывал оператор присваивания перемещения. В следующем примере показана измененная версия конструктора перемещения, вызывающая оператор присваивания перемещения:

В уроке «M.1 – Введение в умные указатели и семантику перемещения» мы рассмотрели std::auto_ptr , обсудили необходимость семантики перемещения и рассмотрели некоторые недостатки, которые возникают, когда функции, разработанные для семантики копирования (конструкторы копирования и операторы присваивания копированием) переопределяются для реализации семантики перемещения.

В этом уроке мы более подробно рассмотрим, как C++11 решает эти проблемы с помощью конструкторов перемещения и присваивания перемещением.

Конструкторы копирования и присваивание копированием

Во-первых, давайте сделаем обзор семантики копирования.

Конструкторы копирования используются для инициализации класса путем создания копии объекта того же класса. Присваивание копированием используется для копирования одного объекта класса в другой существующий объект класса. По умолчанию, если конструктор копирования и оператор присваивания копированием не указаны явно, C++ предоставляет их. Эти предоставляемые компилятором функции создают поверхностные копии, что может вызывать проблемы для классов, динамически выделяющих память. Таким образом, классы, которые имеют дело с динамической памятью, должны переопределять эти функции для создания глубоких копий.

Возвращаясь к нашему примеру класса умного указателя Auto_ptr из первого урока этой главы, давайте рассмотрим версию, которая реализует конструктор копирования и оператор присваивания копированием, которые делают глубокие копии, и пример программы, которая их проверяет:

В этой программе мы используем функцию с именем generateResource() для создания умного указателя, инкапсулирующего ресурс, который затем передается обратно в функцию main() . Затем функция main() присваивает его существующему объекту Auto_ptr3 .

Когда эта программа запускается, она печатает:

Для такой простой программы происходит слишком много созданий и уничтожений объектов Resource ! Что тут происходит?

Короче говоря, поскольку мы вызываем конструктор копирования один раз, чтобы скопировать res во временный объект, и один раз присваивание копированием для копирования временного объекта в mainres , в итоге мы размещаем и уничтожаем в общей сложности 3 отдельных объекта.

Неэффективно, но, по крайней мере, не дает сбоев!

Однако с семантикой перемещения мы можем добиться большего.

Конструкторы перемещения и присваивание перемещением

C++11 определяет две новые функции, обслуживающие семантику перемещения: конструктор перемещения и оператор присваивания перемещением. В то время как цель конструктора копирования и присваивания копированием – выполнить копирование одного объекта в другой, цель конструктора перемещения и присваивания перемещением – передать владение ресурсами от одного объекта к другому (что обычно намного дешевле, чем создание копии).

Определение конструктора перемещения и присваивания перемещением работают аналогично их аналогам для копирования. Однако в то время как копирующие версии этих функций принимают в качестве параметра константную lvalue-ссылку, перемещающие версии этих функций используют в качестве параметра неконстантные rvalue-ссылки.

Вот тот же класс Auto_ptr3 , что и выше, с добавленными конструктором перемещения и оператором присваивания перемещением. Для сравнения мы оставили выполняющие глубокое копирование конструктор копирования и оператор присваивания копированием.

Конструктор перемещения и оператор присваивания перемещением просты. Вместо того, чтобы выполнять глубокое копирование исходного объект ( а ) в неявный объект this , мы просто перемещаем (крадем) ресурсы исходного объекта. Это включает в себя поверхностное копирование указателя исходного объекта в неявный объект this с последующей установкой для указателя исходного объекта значения nullptr .

При запуске эта программа печатает:

Это намного лучше!

Ход программы точно такой же, как и раньше. Однако вместо вызова конструктора копирования и оператора присваивания копированием эта программа вызывает конструктор перемещения и оператор присваивания перемещением. Рассмотрим немного подробнее:

Поэтому вместо того, чтобы копировать наш объект 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% быстрее!

Читайте также: