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

Обновлено: 28.04.2024

В уроке «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% быстрее!

Есть ощущение что я не совсем понимаю (или совсем не понимаю) как работает перемещение (по rvalue ссылкам) в С++, и как правильно следует организовывать/использовать конструкторы/операторы перемещения-копирования в классах.

Немного о сложившейся ситуации

Решил я написать класс "буфер данных", для того чтобы можно было этим самым буфером более удобно манипулировать. По сути состоит из указателя на массив данных, кол-ва элементов в массиве и пары методов. Показываю здесь немного упрощенный вариант, суть от этого особо не меняется:

Затем я попытался сделать примерно такую штуку:

И в итоге получилось так, что указатель data в объекте a более не валидный (а при завершении программы/функции вообще все ломается). По моим предположениям это происходит из-за того что объект, который я получаю путем вызова конструктора A(100) , и который затем присваиваю в a - он как бы временный, и после присвоения тут же срабатывает его деструктор, очищая то, что лежит по указателю data . Получается что значение указателя копируется, а данные уже убиты деструктором (или я не прав?).

Первой мыслью, которая пришла в голову, была "не хватает конструктора копирования". Я решил его написать, выглядел он как-то вот так:

Но это не помогло. Похоже в операции a = A(100); конструктор копирования вовсе не участвует. Немного погуглив - наткнулся на различные идиомы "перемещения/присваивания/копирования", толком не поняв сути, решил что не хватает конструктора перемещения. Добавил его, как-то так он выглядел:

И тут произошло нечто мистическое. Среда начала подчеркивать оператор = как ошибку, там где я пытался сделать a = A(100) , а код вообще перестал компилироваться. Среда давала такие пояснения - "на функцию A::oprator=(A const a&) нельзя ссылаться, так как эта функция удалена". Куда удалена? Почему удалена? Что вообще происходит.. не знаю. В итоге я решил таки переопределить этот самый оператор = , он получился примерно таким:

И о чудо, теперь все работает.

Только вот осталась куча вопросов:

Что я вообще такое сделал? Когда именно вызывается конструктор перемещения а когда конструктор копирования? Почему при переопределении конструктора перемещения A(A&& other):A() перестает работать оператор = и его надо явно определять? В чем смысл в операторе = делать почти то же самое что и в конструкторе перемещения, я ведь мог бы обойтись только оператором перемещения? Как вообще правильно ко всему этому подойти?

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

А использовали бы обычный std::vector (как рекомендует Страуструп) и горя бы не знали, и move семантику даже бы и не изучили.

3 ответа 3

Нашли верное решение - написать всю большую тройку (ныне пятерку :)) копирующий конструктор, деструктор и оператор присвоения. Теперь еще (при необходимости) добавляются перемещающий конструктор и перемещающее присваивание.

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

Когда создается экземпляр. Например,

В новом стандарте есть свои тонкости - так, в

копирующий/перемещающий конструктор не вызывается.

Почему при переопределении конструктора перемещения A(A&& other):A() перестает работать оператор = и его надо явно определять?

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

В чем смысл в операторе = делать почти то же самое что и в конструкторе перемещения, я ведь мог бы обойтись только оператором перемещения?

Вряд ли в ситуации a = b; можно обойтись оператором перемещающего присваивания - тогда b у вас был бы пустым после присваивания.

Но есть распространенная идиома присваивания через копирование и обмен - если есть функция swap , обменивающая внутренние представления двух объектов (вот тут - просто меняя указатели), то присваивание можно сделать как

Как вообще правильно ко всему этому подойти?

Почитать соответствующую литературу. Например, Мейерса "Эффективный и современный C++". Большой список есть здесь.

Есть ощущение что я не совсем понимаю (или совсем не понимаю) как работает перемещение (по rvalue ссылкам) в С++

Тут сразу можно заметить, что "перемещение" в С++ работает так, как вы его сами реализуете. В самом языке (в ядре языка) никакого "перемещения" нет. Есть только тип rvalue-ссылки со своими правилами поведения в процессе разрешения перегрузок (overload resolution). А уж воспользоваться этим типом rvalue-ссылки и сопутствующими ему правилами разрешения перегрузок для целей "перемещения" - ваша задача.

Однако вас никто не заставляет это делать. Перемещение - это во многих случаях лишь оптимизационная возможность. При этом с концептуальной точки зрения обычное "копирование" - это частный случай "перемещения". "Копирование" есть наименее оптимальный вариант "перемещения". То есть вас никто никогда не заставляет реализовывать перемещение там, где у вас уже реализовано копирование. Копирование и так само со всем справится, пусть и менее оптимально.

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

Затем я попытался сделать примерно такую штуку:

[. ] он как бы временный, и после присвоения тут же срабатывает его деструктор, очищая то, что лежит по указателю data. Получается что значение указателя копируется, а данные уже убиты деструктором (или я не прав?).

Все совершенно верно.

Первой мыслью, которая пришла в голову, была "не хватает конструктора копирования".

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

Именно поэтому ваша реализация конструктора копирования (вполне корректная для наших целей) ситуации не спасла. Как вы сами правильно заметили, конструктор копирования в вашем коде не используется вообще.

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

решил что не хватает конструктора перемещения.

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

Это так называемое классическое Правило Трех.

Добавил его [. ] Среда давала такие пояснения - "на функцию A::oprator=(A const a&) нельзя ссылаться, так как эта функция удалена". Куда удалена? Почему удалена? Что вообще происходит..

В современном С++ бытует вполне резонное мнение, что языку изначально следовало бы более строго требовать от пользователя соблюдения Правила Трех. А именно: в ситуациях, когда пользователь объявляет в своем классе хотя бы одну из функций Правила Трех (конструктор копирования, копирующий оператор присваивания, деструктор), автоматически подавлять неявную генерацию всех остальных функций Правила Трех. То есть язык должен заставлять пользователя действовать по принципу "реализовал руками одну - тогда реализуй руками и все остальные". Однако в классическом С++ этого сделано не было. Вы как раз стали жертвой этой ситуации: вы реализовали конструктор копирования и деструктор, но "забыли" реализовать соответствующий им оператор присваивания.

Именно это вы и наблюдаете: как только вы реализовали свой конструктор перемещения, сгенерированный компилятором копирующий оператор присваивания сразу же пропал - он стал deleted. И ваш код перестал компилироваться.

В итоге я решил таки переопределить этот самый оператор =, он получился примерно таким:

Прекрасно. При этом для достижения формальной работоспособности кода вам на самом деле "не нужен" ваш конструктор перемещения. Но для оптимизации он вполне полезен.

То, как вы реализовали ваш оператор присваивания (получение параметра "по значению" и swap ) как раз является наиболее простым способом оптимизировать код на основе семантики перемещения.

Зачем нужен конструктор перемещения, если есть оператор перемещения( operator=(T&&) )?

Это вопрос? Когда в качестве аргумента передается const Type& (еще возможна ситуация с Type x = y , так называемая, copy-initialization). Здесь можно прочитать подробнее

3 ответа 3

Область использования

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

  • конструктор перемещения применяется в местах, где объявление совпадает с определением (инициализацией) rvalue-ссылкой на экземпляр этого же класса, либо посредством direct initialization в конструкторе класса/структуры (если же определение произойдет с помощью lvalue-ссылки, то вызовется конструктор копирования);
  • оператор перемещения применяется в местах, где экземпляр класса уже был ранее определен и к нему применяется operator = , который в качестве аргумента приминает rvalue-ссылку на экземпляр этого же класса (если же оператор принимает lvalue-ссылку , то вызовется оператор присваивания).

Про rvalue-ссылки можете почитать здесь, здесь и здесь.

Контрольный пример (для разъяснения отличия в работе данных конструкций)

Дополнение

В C++11 каждый класс, помимо конструктора по умолчанию, имеет следующие 5 дефолтных операций:

  1. конструктор копирования (copy constructor);
  2. оператор присваивания (copy assignment);
  3. конструктор перемещения (move constructor);
  4. оператор перемещения (move assignment);
  5. деструктор (destructor).

При определении одной из этих 5-ти операций рекомендуется явно указать (либо определить, либо объявить с помощью default или delete ) все остальные, т.к. все эти 5 операций тесно связаны. Это будет способствовать лучшему пониманию семантики класса при чтении кода.
Если явно определена одна из упомянутых 5-ти операций (в том числе с использованием default или delete ), то:

  • недостающие операции копирования будут определены автоматически с поведением по умолчанию;
  • недостающие операции перемещения определены не будут.

Это следует учитывать при написании классов.

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

Конструктор в общем случае работает на "сыром" (несконструированном, непроинициализорованном) блоке памяти. В момент начала работы конструктора объекта как такового еще не существует и он не имеет никакого предсказуемого состояния. Соответственно работа конструктора сводится к созданию/инициализации нового объекта в предоставленном блоке "сырой" памяти. Конструктор копирования, например, копирует это состояние из некоего объекта-образца, конструктор перемещения - перемещает, конструктор преобразования - преобразует и т.д. Конструктор перемещения никоим образом не выделяется из этого ряда.

Оператор присваивания же всегда имеет дело с уже проинициализированным/сконструированным объектом, находящимся в некоем предсказуемом "валидном" состоянии. Работа оператора присваивания сводится к освобождению исходного состояния объекта (освобождению ресурсов, например), за которым следует копирование (или перемещение, или преобразование и т.п.) нового состояния из некоего объекта-источника.

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

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

В принципе, стандартная реализация оператора присваивания могла бы делать следующее: для объекта в левой части оператора присваивания вызвать сначала деструктор, а потом конструктор копирования, передав ему ссылку на объект в правой части. Наверное, это было бы лучше, чем просто копировать все поля из правого объекта в левый, но всё равно это было бы не совсем правильно. Дело в том, что при копировании объекта требуется выделить новый ресурс, но потенциально это может закончиться неудачей. Если это произойдёт, то после неудачной попытки присваивания объект в левой части останется в некорректном состоянии: старый ресурс уже освобождён (вызван деструктор), а новый захватить не получилось. Поэтому при перегрузке оператора присваивания нужно всегда сначала захватывать новый ресурс, и только если эта операция прошла успешно, освобождать старый, заменяя его новым (по этой причине оператор присваивания в принятом ответе написан не совсем правильно). В случае перемещения, в принципе, такой проблемы нет: копируется только ссылка на ресурс, и старый ресурс, которым владел объект в левой части оператора перемещения, может быть освобождён безболезненно, как до копирования ссылки, так и после. Скорее всего, аналогичное копированию разделение на конструктор и оператор в случае перемещения сделано для случая, когда неудачей закончилось освобождение ресурса в левой части оператора перемещения. В этой ситуации необходимо, чтобы объект в правой части продолжал владеть ресурсом. Таким образом сохраняется атомарность копирования и перемещения: либо нам удалось полностью создать копию объекта или полностью переместить ресурс из одного объекта в другой, либо копирование / перемещение прошло неудачно, и ни один из объектов не был изменён.

Всё ещё ищете ответ? Посмотрите другие вопросы с метками c++ c++11 или задайте свой вопрос.

Связанные

Похожие

Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.

Site design / logo © 2022 Stack Exchange Inc; user contributions licensed under cc by-sa. rev 2022.6.23.42447

Нажимая «Принять все файлы cookie», вы соглашаетесь, что Stack Exchange может хранить файлы cookie на вашем устройстве и раскрывать информацию в соответствии с нашей Политикой в отношении файлов cookie.

Чтобы настроить, как класс инициализирует его члены или вызывать функции при создании объекта класса, определите конструктор. Конструкторы имеют имена, совпадающие с именами классов, и не имеют возвращаемых значений. Вы можете определить столько перегруженных конструкторов, сколько необходимо для настройки инициализации различными способами. Как правило, конструкторы имеют открытые специальные возможности, чтобы код за пределами определения класса или иерархии наследования может создавать объекты класса. Но вы также можете объявить конструктор как 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 г. в этом примере выводятся следующие выходные данные:

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

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

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

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

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

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