для чего нужен конструктор копирования

BestProg

С++. Конструктор копирования. Примеры использования. Передача объекта класса в функцию. Возврат класса из функции

В данной теме рассмотрена работа конструктора копирования на примере unmanaged ( native ) классов. При рассмотрении данной темы рекомендуется прочитать тему:

Содержание

Поиск на других ресурсах:

1. Какое назначение конструктора копирования ( copy constructor )?

Конструктор копирования – это специальный конструктор, который позволяет получить идентичный к заданному объект. То есть, с помощью конструктора копирования можно получить копию уже существующего объекта. Конструктор копирования еще называется инициализатором копии ( copy initializer ). Конструктор копирования должен получать входным параметром константную ссылку ( & ) на объект такого же класса.

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

Конструктор копирования вызывается в случаях, когда нужно получить полную копию объекта. В C++ полная копия объекта нужна в трех случаях.

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

Случай 3. Когда нужно вернуть объект из функции по значению. В этом случае также создается полная копия объекта.

3. В каких случаях целесообразно использовать конструктор копирования?

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

4. Пример объявления конструктора копирования в классе, где нет динамического выделения памяти

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

Демонстрация использования конструктора копирования в некотором программном коде (методе)

5. Пример передачи объекта класса в функцию как параметр-значение

Использование функции в другом программном коде

6. Пример возврата объекта класса из функции по значению с помощью конструктора копирования

Объявление класса точно такое же как в п. 4.

Демонстрация использования функций

7. Как осуществляется копирование объектов, когда в классе отсутствует конструктор копирования?

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

Побитовое копирование есть приемлемым для классов, в которых нет динамического выделения памяти. Однако, если в классе есть динамическое выделение памяти (класс использует указатели), то побитовое копирование приведет к тому, что указатели обоих объектов будут указывать на один и тот же участок памяти. А это ошибка.

Источник

Конструкторы копий и операторы присваивания копий (C++)

Начиная с C++ 11, в языке поддерживаются два вида присваивания: копирование назначения и Перемещение. В этой статье «присваивание» означает «присваивание копированием», если явно не указано другое. Сведения о назначении Move см. в разделе конструкторы Move и операторы присваивания перемещения (C++).

Как при операции назначения, так и при операции инициализации выполняется копирование объектов.

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

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

Можно определить семантику копии объектов типа класса. Рассмотрим для примера такой код:

Приведенный выше код может означать «копировать содержимое файла FILE1. DAT в FILE2. DAT» или это может означать «Ignore FILE2. DAT и создайте b второй обработчик для file1. dat. » Необходимо прикрепить соответствующую семантику копирования к каждому классу, как показано ниже.

С помощью конструктора копии.

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

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

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

Дополнительные сведения о перегруженных операторах присваивания см. в разделе назначение.

Источник

Для чего нужен конструктор копирования

1. Конструктор копирования

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

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

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

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

2. Перегруженная операция присваивания

Перегруженная операция присваивания используется при присваивании одного объекта другому существующему объекту. Здесь присутствует такая же проблема, что и в конструкторе копирования. К тому же, у объекта, которому присваивается значение, уже может быть выделена динамическая память. Перед присваиванием новых данных, выделенную ранее память необходимо очистить, чтобы не допустить её утечки (см. пример в конце). Также необходимо обработать случай самоприсваивания. В противном случае, данные в динамической памяти просто будут утеряны. Аналогично копированию, присваивание также можно запретить, поместив операцию в приватной области класса.

3. Деструктор

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

4. Пример

Стоить отметить, что во всех трёх функциях память должна выделяться и удаляться одинаковым образом. Т. е. нельзя в одном случае использовать delete, а в другом delete[].

Источник

Конструкторы (C++)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Это пример проблемы Most Vexing Parse (наиболее неоднозначного анализа). Поскольку выражение примера можно интерпретировать как объявление функции или как вызов конструктора по умолчанию и в связи с тем, что средства синтаксического анализа C++ отдают предпочтение объявлениям перед другими действиями, данное выражение обрабатывается как объявление функции. Дополнительные сведения см. в разделе досадной Parse.

В случае явного объявления конструкторов компилятор не предоставляет конструктор по умолчанию:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Порядок создания

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Источник

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

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

Оглавление

Введение

Объектно-ориентированные возможности C++ естественно приводит к следующему решению: класс, управляющий ресурсом, содержит дескриптор ресурса в качестве члена, инициализирует дескриптор при захвате ресурса и освобождает ресурс в деструкторе. Но после некоторых размышлений (или опыта) приходит понимание того, что не все так просто. И главной проблемой является семантика копирования. Если класс, управляющий ресурсом, использует копирующий конструктор, сгенерированный компилятором по умолчанию, то после копирования объекта мы получим две копии дескриптора одного и того же ресурса. Если один объект освобождает ресурс, то после этого второй сможет совершить попытку использования или освобождения уже освобожденного ресурса, что в любом случае является не корректным и может привести к так называемому неопределенному поведению, то есть может произойти все, что угодно, например аварийное завершение программы.

К счастью, в C++ программист может полностью контролировать процесс копирования путем собственного определения копирующего конструктора и оператора копирующего присваивания, что позволяет решить вышеописанную проблему, причем обычно не одним способом. Реализация копирования должна быть тесно увязана с механизмом освобождения ресурса, и это все вместе будем называть стратегией копирования-владения. Хорошо известно так называемое «правило большой тройки», которое утверждает, что если программист определил хотя бы одну из трех операций — копирующий конструктор, оператор копирующего присваивания или деструктор, — то он должен определить все три операции. Стратегии копирования-владения как раз и конкретизируют, как это надо делать. Существует четыре основных стратегии копирования-владения.

1. Основные стратегии копирования-владения

1.1. Стратегия запрета копирования

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

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

Стандарт C++11 предлагает для этого случая специальный синтаксис:

Этот синтаксис более нагляден и дает более понятные сообщения компилятора при попытке копирования.

1.2. Стратегия исключительного владения

В этом случае при реализации копирования и присваивания дескриптор ресурса перемещается от объекта-источника к целевому объекту, то есть остается в единственном экземпляре. После копирования или присваивания, объект-источник имеет нулевой дескриптор и не может использовать ресурс. Деструктор освобождает захваченный ресурс. Для этой стратегии также используются термины эксклюзивное или строгое владение [Josuttis], Андрей Александреску [Alexandrescu] использует термин разрушающее копирование. В C++11 это делается следующим образом: запрещается обычное копирование и копирующее присваивание вышеописанным способом, и реализуются семантики перемещения, то есть определяются перемещающий конструктор и оператор перемещающего присваивания. (Подробнее о семантике перемещения далее.)

Таким образом, стратегию исключительного владения можно считать расширением стратегии запрета копирования.

1.3. Стратегия глубокого копирования

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

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

Стратегия глубокого копирования используется во всех типах объектных строк, std::vector<> и других контейнерах стандартной библиотеки.

1.4. Стратегия совместного владения

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

2. Стратегия глубокого копирования — проблемы и решения

Рассмотрим шаблон функции обмена состояниями объектов типа T в стандартной библиотеке C++98.

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

2.1. Копирование при записи

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

1. Определить в классе функцию-член Swap() (имя не принципиально), реализующую обмен состояниями.

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

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

Функция-член Swap() определяется обычно легко: необходимо последовательно применять к базам и членам операцию обмена состояниями, если они ее поддерживают, и std::swap() в противном случае.

Приведенное описание несколько упрощено, более детальное можно найти в [Meyers2]. Обсуждение проблем, связанных с функцией обмена состояниями, также можно найти в [Sutter/Alexandrescu].

Функцию обмена состояниями можно отнести к одной из базовых операций класса. С помощью нее можно изящно определить другие операции. Например, оператор копирующего присваивания определяется через копирование и Swap() следующим образом:

Этот шаблон называется идиомой «копирование и обмен» или идиомой Герба Саттера, подробнее см. [Sutter], [Sutter/Alexandrescu], [Meyers2]. Его модификацию можно применить для реализации семантики перемещения, см. разделы 2.4, 2.6.1.

2.3. Удаление промежуточных копий компилятором

Компиляторы могут удалять промежуточные копии и в других ситуациях.

2.4. Реализация семантики перемещения

Реализация семантики перемещения заключается в определении перемещающего конструктора, имеющего параметр типа rvalue-ссылка на источник и оператора перемещающего присваивания с таким же параметром.

В стандартной библиотеке C++11 шаблон функции обмена состояниями определен следующим образом:

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

Операции реализуются путем последовательного применения операции перемещения к базам и членам класса, если они поддерживают перемещение, и операции копирования в противном случае. Понятно, что такой вариант далеко не всегда приемлем. Сырые дескрипторы не перемещаются, но копировать их обычно нельзя. При выполнении определенных условий компилятор может самостоятельно сгенерировать подобный перемещающий конструктор и оператор перемещающего присваивания, но этой возможностью лучше не пользоваться, условия эти довольно запутаны и легко могут измениться при доработке класса. Подробнее см. [Meyers3].

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

Правило 2 обсуждалось выше. Правило 4 связано с тем, что именованные rvalue-ссылки являются lvalue (см. также Приложение А). Это можно проиллюстрировать на примере определения перемещающего конструктора.

Другой пример этого правила приведен выше, при определении оператора перемещающего присваивания. Реализация семантики перемещения рассматривается также в разделе 6.2.1.

2.5. Размещение vs. вставки

Размещение имеет следующие преимущества:

Приведем пример, где одна и та же задача решается разными способами.

2.6. Итоги

Одной из главных проблем классов, реализующих стратегию глубокого копирования, является создание временных копий ресурса. Ни один из описанных способов полностью не решает эту проблему и полностью не замещает какой-то другой способ. В любом случае программист должен распознавать подобные ситуации и писать правильный код с учетом описанной проблемы и возможностей языка. Простейший пример — это передача параметров в функцию: передавать надо по ссылке, а не по значению. Эта ошибка не распознается компилятором, но при этом происходит либо ненужное копирование, либо программа работает не так, как задумано. Другой пример связан с использованием перемещения: программист должен четко соблюдать условия, при которых компилятор выбирает перемещение, иначе «молча» будет использовано копирование.

Если все-таки при реализации класса-владельца ресурса принято решение использовать стратегию глубокого копирования, то кроме реализации семантики копирования можно рекомендовать следующие шаги:

3. Возможные варианты реализации стратегии совместного владения

В стандартной библиотеке C++11 интеллектуальный указатель std::shared_ptr<> также использует счетчик ссылок. Но объект, контролируемый этим интеллектуальным указателем, может не иметь внутреннего счетчика ссылок, поэтому создается специальный скрытый объект, называемый управляющим блоком, который управляет счетчиком ссылок. Понятно, что это является дополнительным накладным расходом. Интеллектуальный указатель std::shared_ptr<> подробно описан в [Josuttis], [Meyers3].

Андрей Александреску рассматривает реализацию стратегии совместного владения с помощью двусвязного списка объектов-владельцев [Alexandrescu]. Герберт Шилдт описывает (и приводит полный код) реализации, основанной на комбинации двусвязного списка и счетчика ссылок [Schildt]. Реализации на основе двусвязного списка также не могут освободить ресурсы, имеющие циклические ссылки.

Описание более сложных схем удаления неиспользуемых объектов (сборщиков мусора) можно найти в [Alger].

Реализация стратегии совместного владения также должна учитывать возможность многопоточного доступа к объектам-владельцам. Эта тема обсуждается в [Josuttis] и [Alexandrescu].

4. Стратегия исключительного владения и семантика перемещения

5. Стратегия запрета копирования — быстрое начало

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

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

6. Жизненный цикл ресурса и объекта-владельца ресурса

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

6.1. Захват ресурса при инициализации

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

Конструкторы таких классов обычно имеют параметры, необходимые для захвата ресурса и, соответственно, конструктор по умолчанию отсутствует. В стандартной библиотеке C++11 таким образом реализованы некоторые классы для поддержки многопоточной синхронизации.

Эта схема управления ресурсом является одним из вариантов идиомы «захват ресурса при инициализации» (resource acquisition is initialization, RAII). Идиома RAII широко обсуждается во многих книгах и в интернете (и часто трактуется немного по разному или просто не вполне четко), см., например [Dewhurst1]. Приведенный выше вариант можно назвать «строгим» RAII. В таком классе дескриптор ресурса естественно сделать константным членом, и, соответственно, можно использовать термин неизменяемое (immutable) RAII.

6.2. Расширенные варианты управления жизненным циклом ресурса

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

6.2.1. Расширенный жизненный цикл ресурса

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

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

После этого расширенный жизненный цикл ресурса реализуется совсем просто.

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

Определение перемещающего конструктора и оператора перемещающего присваивания:

Определение отдельных функций-членов захвата и освобождения ресурса:

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

В приведенных выше примерах определения оператора копирующего присваивания и функции-члена захвата ресурса использовалась идиома «копирование и обмен», в соответствии с которой сначала захватывается новый ресурс, потом освобождается старый. Эта схема обеспечивает так называемую строгую гарантию безопасности исключений: если при захвате ресурса произошло исключение, то объект останется в том же состоянии, что и до начала операции (транзакционная семантика). В определенных ситуациях может оказаться более предпочтительной другая схема: сначала освобождается старый ресурс, затем захватывается новый. Такой вариант обеспечивает более слабую гарантию безопасности исключений, называемую базовой: если при захвате ресурса произошло исключение, то объект уже не обязательно останется в том же состоянии, но новое состояние будет корректным. Кроме того, при определении оператора копирующего присваивания по этой схеме необходима проверка на самоприсваивание. Подробнее гарантии безопасности исключений обсуждаются в [Sutter], [Sutter/Alexandrescu], [Meyers2].

Итак, переход от RAII к расширенному жизненному циклу ресурса очень похож на переход от стратегии запрета копирования к стратегии исключительного владения.

6.2.2. Однократный захват ресурса

Этот вариант можно рассматривать как промежуточный между RAII и расширенным жизненным циклом ресурса. Будем говорить, что класс, управляющий ресурсом, использует однократный захват ресурса, если для него выполнены следующие условия:

Это «почти» RAII, единственное отличие — это возможность формального разделения операции создания объекта и захвата ресурса. Такой класс может иметь перемещающий конструктор, но не оператор перемещающего присваивания, иначе нарушится условие п. 3. Это упрощает хранение объектов в стандартных контейнерах. Несмотря на некоторую «недоделанность», данный вариант достаточно практичен.

6.2.3. Повышение уровня косвенности

Другой подход к расширению жизненного цикла ресурса — это повышение уровня косвенности. В этом случае сам объект RAII рассматривается как ресурс, а указатель на него будет дескриптором ресурса. Захват ресурса сводится к созданию объекта в динамической памяти, а освобождение к его удалению. В качестве класса, управляющим таким ресурсом, можно использовать один из интеллектуальных указателей стандартной библиотеки или аналогичный по функционалу класс (подобные классы называют классами-дескрипторами). Стратегия копирования-владения определяется интеллектуальным указателем или легко реализуется (для класса-дескриптора). Этот способ значительно проще описанного в разделе 6.2.1, единственный недостаток заключается в более интенсивном использовании динамической памяти.

6.3. Совместное владение

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

7. Итоги

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

Существует 4 основные стратегии копирования-владения:

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

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

Приложения

Приложение А. Rvalue-ссылки

Для примеров будем использовать класс:

Как и обычные ссылки, rvalue-ссылки необходимо инициализировать.

Первым отличием rvalue-ссылок от обычных С++ ссылок заключается в том, что их нельзя инициализировать с помощью lvalue. Пример:

Для корректной инициализации необходимо использовать rvalue:

или lvalue должно быть явно приведено к типу rvalue-ссылки:

Rvalue ссылки можно инициализировать с помощью rvalue встроенного типа, для обычных ссылок это запрещено.

После инициализации rvalue-ссылки можно использовать как обычные ссылки.

Rvalue-ссылки неявно приводятся к обычным ссылкам.

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

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

Функция с параметром, передаваемым по значению, и перегруженная версия, имеющая параметр типа rvalue-ссылка, будут неразрешимы (ambiguous) для rvalue аргументов.

Для примера рассмотрим перегруженные функции

и несколько вариантов их вызова

Следует обратить внимание на один важный момент: именованная rvalue-ссылка сама по себе является lvalue.

Приложение Б. Семантика перемещения

Список литературы

[Alexandrescu]
Александреску, Андрей. Современное проектирование на C++.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2002.

[Guntheroth]
Гантерот, Курт. Оптимизация программ на C++. Проверенные методы для повышения производительности.: Пер. с англ. — СПб.: ООО «Альфа-книга», 2017.

[Josuttis]
Джосаттис, Николаи М. Стандартная библиотека C++: справочное руководство, 2-е изд.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2014.

[Dewhurst1]
Дьюхерст, Стивен С. C++. Священные знания, 2-е изд.: Пер. с англ. — СПб.: Символ-Плюс, 2013.

[Dewhurst2]
Дьюхэрст, Стефан К. Скользкие места C++. Как избежать проблем при проектировании и компиляции ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2012.

[Meyers1]
Мейерс, Скотт. Наиболее эффективное использование C++. 35 новых рекомендаций по улучшению ваших программ и проектов.: Пер. с англ. — М.: ДМК Пресс, 2000.

[Meyers2]
Мейерс, Скотт. Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2014.

[Meyers3]
Мейерс, Скотт. Эффективный и современный C++: 42 рекомендации по использованию C++11 и C ++14.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2016.

[Sutter]
Саттер, Герб. Решение сложных задач на C++.: Пер. с англ. — М: ООО «И.Д. Вильямс», 2015.

[Sutter/Alexandrescu]
Саттер, Герб. Александреску, Андрей. Стандарты программирования на С++.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2015.

[Schildt]
Шилдт, Герберт. Искусство программирования на C++.: Пер. с англ. — СПб.: БХВ-Петербург, 2005.

[Alger]
Элджер, Джефф. C++: библиотека программиста.: Пер. с англ. — СПб.: ЗАО «Издательство «Питер», 1999.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *