Оглавление
Оглавление
1. Объявление и использование специальных функций-членов
1.1. Объявление
1.2. Контекст вызова
1.2.1. Конструкторы
1.2.2. Операторы присваивания
1.2.3. Деструкторы
1.3. Спецификатор доступа
1.4. Объявление виртуальными
2. Генерация компилятором специальных функций-членов
2.1. Удаленные специальные функции-члены
2.2. Условия генерации компилятором специальных функций-членов
2.3. Объявление специальных функций-членов с реализацией, сгенерированной компилятором
2.4. Алгоритм генерации компилятором специальных функций-членов
3. Конструкторы и деструктор
3.1 Инициализация и тривиальные типы
3.1.1. Общая схема инициализации
3.1.2. Тривиальные типы
3.1.3. Ст��тическая память
3.1.4. Конструктор по умолчанию, сгенерированный компилятором
3.1.5. Конструктор, определенный программистом
3.1.6. Список инициализации конструктора
3.1.7. Инициализатор при объявлении члена
3.1.8. Непустой инициализатор
3.2 Деструктор
4. Специальные функции-члены, определяемые программистом
5. Объединения
5.1. Инициализация
5.2. Копирующий конструктор, оператор копирующего присваивания, деструктор
5.3. Выводы
6. Список статей серии «C++, копаем вглубь»
7. Итоги
Список литературы
# 1. Объявление и использование специальных функций-членов
## 1.1. Объявление К специальным функциям-членам (special member functions) относятся 6 функций: конструктор по умолчанию (default constructor), деструктор (destructor), копирующий конструктор (copy constructor), оператор копирующего присваивания (copy assignment operator), перемещающий конструктор (move constructor) и оператор перемещающего присваивания (move assignment operator). Последние две появились в C++11. (Подробнее о семантике перемещения можно почитать у Скотта Мейерса [Meyers].)
Имена и сигнатуры специальных функций-членов подчиняются определенным правилам. Сначала рассмотрим пример объявления:
class Demo
{
public:
Demo(); // конструктор по умолчанию
~Demo(); // деструктор
//
Demo(const Demo& src); // копирующий конструктор
Demo& operator=(const Demo& src); // оператор
// копирующего присваивания
Demo(Demo&& src) noexcept; // перемещающий конструктор
Demo& operator=(Demo&& src) noexcept; // оператор
// перемещающего присваивания
// ...
};
Конструкторы имеют имя, совпадающее с именем класса, деструктор дополнительно имеет префикс ~. Конструкторы и деструктор не имеют возвращаемого значения. Конструктор по умолчанию и деструктор не имеют параметров. Для операторов присваивания стандартным возвращаемым значением является ссылка на результат присваивания (для возврата обычно используется инструкция return *this;). Это позволяет использовать цепочку присваиваний в одной инструкции, именно так работают встроенные операторы присваивания. Для копирующих функций-членов типом параметра должна быть ссылка на константу того же класса, а для перемещающих — rvalue ссылка. Перемещающие функции-члены рекомендуется объявлять как noexcept, это позволяет компилятору лучше оптимизировать код.
Специальные функции-члены можно объявлять в классах, структурах и объединениях. Объединения имеют ряд особенностей и будут рассмотрены отдельно в разделе 5, поэтому материалы, изложенные в разделах 2-4, будут относиться в основном к классам и структурам.
## 1.2. Контекст вызова Специальные функции-члены могут использоваться не так, как обычные функции-члены, в ряде случаев программист не может их явно вызывать в своем коде, эти вызовы генерируются компилятором в определенном контексте.
### 1.2.1. Конструкторы Компилятор выполняет вызов конструктора (любого, а не только по умолчанию) при реализации инструкции объявления переменной или использования оператора `new`. Компилятор выполняет вызов копирующего или перемещающего конструктора при реализации передачи параметров функций по значению и при реализации возвращаемого значения функции. Вот примеры: ```cpp Demo x{}; // конструктор по умолчанию Demo *p = new Demo(); // конструктор по умолчанию Demo a[8]; // конструктор по умолчанию Demo y{x}; // копирующий конструктор Demo z{std::move(x)}; // перемещающий конструктор
Demo Foo()
{
Demo x;
// ...
return x; // копирующий или перемещающий конструктор
}
<anchor>id-1-2-2</anchor>
### 1.2.2. Операторы присваивания
Компилятор использует операторы присваивания для реализации присваивания объектов. Вот примеры:
```cpp
Demo x, y, z;
x = y; // оператор копирующего присваивания
z = std::move(x); // оператор перемещающего присваивания
Для выполнения присваивания можно использовать так называемую функциональную форму оператора присваивания. В этом случае вместо выражения x=y (инфиксная форма) используется выражение x.operator=(y) (функциональная форма). Функциональная форма позволяет разрешить некоторые неоднозначности, возникающие при использовании инфиксной формы, например, вызвать оператор базового класса.
Обратим внимание на отличие присваивания от инициализации, инициализация выполняется при создании объекта, а присваивание применяется к ранее созданному и уже инициализированному объекту. (Некоторая путаница может происходить из-за того, что для инициализации иногда применяют символ =.)
### 1.2.3. Деструктор Деструктор вызывается компилятором при уничтожении объекта. Для объектов, объявленных в локальной области видимости, уничтожение происходит при выходе переменной из области видимости. Для динамических объектов и элементов динамических массивов при вызове оператора `delete` или `delete[]` соответственно. Для статических членов класса, а также объектов, объявленных глобально или в области видимости пространства имен, уничтожение происходит в на определенной стадии завершения программы. Во всех этих случаях явный вызов деструктора запрещен.
Но вообще, синтаксис C++ не запрещает явный вызов деструктора и, более того, в ряде случаев это необходимо. Одной из таких ситуаций является использование размещающего оператора new (placement new) для инициализации объекта. В этом случае объект инициализируется в памяти, которая заранее выделена для размещения объекта. Такой объект нельзя уничтожать оператором delete, для его уничтожения необходимо явно вызвать деструктор, после чего можно освобождать память или использовать ее повторно.
На описанных свойствах деструктора базируются все стратегии управления ресурсами.
## 1.3. Спецификаторы доступа В приведенном выше примере специальные функции-члены объявлены как общедоступные (`public`). Но это в общем случае не обязательно, достаточно, чтобы специальная функция-член была доступна в контексте вызова.
Для примера рассмотрим, какие требования по доступу должны быть у конструктора и деструктора. Если объявлять переменную глобально или в области видимости пространства имен, то конструктор и деструктор должны быть общедоступные. В классе для типа члена и базового класса конструктор и деструктор должны быть доступны в контексте класса. Для локальных переменных конструктор и деструктор должны быть доступны в блоке, где объявлена переменная. Для динамических объектов конструктор должен быть доступным в контексте вызова оператора new, а деструктор в контексте вызова оператора delete или delete[].
Если проектируется класс, который планируется использовать только как базовый для других классов, то специальные функции-члены можно объявлять как защищенные (protected), в этом случае они будут доступны в контексте производных классов. Такое решение будет лучше отражать проектный замысел и предохранять от неправильного использования.
Если проектируется класс, экземпляры которого создаются только динамически, то появляется дополнительная возможность использовать защищенные или даже закрытые (private) специальные функции-члены — контекст их вызова должен находиться в общедоступных функциях-членах класса. Вот пример:
class X
{
protected:
X();
~X();
public:
static X* CreateInstance(){ return new X(); }
void Delete() { delete this; }
};
В этом случае динамический объект типа X нельзя будет создать с помощью оператора new, и удалить с помощью оператора delete вне контекста класса, надо использовать соответствующие функции-члены. Подобные решения используются при разработке так называемых интерфейсных классов.
Напомним, что имеется дополнительная возможность управления доступом — в классе могут быть объявлены дружественные (friend) функции и классы. Друзья получают полный доступ ко всем членам и функциям-членам класса, который их объявил.
## 1.4. Объявление виртуальными По правилам C++ конструкторы нельзя объявлять виртуальными. (Можно встретить термин «виртуальный конструктор», но это только название для некоторых паттернов проектирования.) Операторы присваивания формально можно объявлять виртуальными, но это не рекомендуется делать, так как присваивание, по сути, не полиморфная операция (эта тема обсуждается, например, Стефаном Дьюхэрстом в [Dewhurst]). А вот деструктор не только можно, но и в некоторых случаях нужно объявлять виртуальным. Если проектируется полиморфная иерархия классов, то в базовом классе деструктор следует объявить виртуальным. В этом случае оператор `delete` будет работать полиморфно, ему в качестве аргумента можно передавать указатель на базовый класс и все равно будет вызван деструктор фактического типа объекта, на который этот указатель указывает.
# 2. Генерация компилятором специальных функций-членов Важная особенность специальных функций-членов — это то, что в ряде случаев программист может их не определять, они будут сгенерированы компилятором (compiler generated member functions).
## 2.1. Удаленные специальные функции-члены Сначала опишем, как можно запретить генерацию компилятором специальных функций-членов. В C++11 появилась возможность объявить функцию или функцию-член удаленной (deleted), для этого ее надо объявить со спецификатором `=delete`. Если объявить удаленной специальную функцию-член, то компилятор не будет генерировать эту функцию-член. Вот пример: ```cpp class X { public: X(const X&) = delete; X& operator=(const X&) = delete; // ... }; ``` Для этого класса генерация копирующего конструктора и оператора копирующего присваивания будет запрещена и, соответственно, копирование и присваивание экземпляров класса станут невозможными.
Компилятор в ряде случаев самостоятельно объявляет некоторые функции-члены удаленными, например, оператор копирующего присваивания и оператор перемещающего присваивания будут объявлены удаленными, если в классе есть нестатические члены типа константа или ссылка. Другие примеры будут приведены ниже. Явное определение или объявление такой функции-члена программистом будет перекрывать неявное объявление удаленной.
## 2.2. Условия генерации компилятором специальных функций-членов Если специальная функция-член не объявлена удаленной и не определена программистом, то при выполнении некоторых условий, компилятор может самостоятельно сгенерировать эту функцию-член. Главное условие для генерации компилятором такой функции-члена — это реальная необходимость в ее использовании, то есть наличие контекста, в котором требуется ее вызов (см. раздел 1.2).
Есть еще некоторые дополнительные условия, зависящие от объявления других функций-членов. Эти условия различны для каждой специальной функции-члена. Вот эти дополнительные условия.
Для конструктора по умолчанию требуется отсутствие объявлений других конструкторов.
Для деструктора, копирующего конструктора, оператора копирующего присваивания специальных условий нет.
Для перемещающего конструктора, оператор перемещающего присваивания требуется, чтобы в классе не были объявлены ни копирующий конструктор, ни оператор копирующего присваивания, ни деструктор. Кроме того, требуется, чтобы перемещающий конструктор и оператор перемещающего присваивания были не объявлены одновременно (то есть генерация возможна только для обеих функций-членов одновременно).
Если эти условия выполнены, то компилятор будет пытаться генерировать такую функцию-член, но эта попытка не обязательно завершается успешно, результат зависит от членов и базовых классов, подробнее см. раздел 2.4.
Если генерация функции-члена прошла успешно, то она будет общедоступна и встраиваемая. В случае неуспеха, для перемещающих операций будет попытка заменить их на соответствующие копирующие, в остальных случаях будет ошибка.
## 2.3. Объявление специальных функций-членов с реализацией, сгенерированной компилятором В C++11 появилась полезная возможность — можно объявить специальную функцию-член, но не предоставлять ее реализацию, а указать, что реализация должна быть сгенерирована компилятором. Для этого функцию-член надо объявить со спецификатором `=default`. По-прежнему компилятор будет пытаться генерировать такую функцию-член только, если она реально используется, но вот выполнения дополнительных условий из раздела 2.2. уже не требуется. Попытка генерации не всегда завершается успешно, подробнее см. раздел 2.4.
Если функция-член может быть сгенерирована компилятором, то такое объявление не является обязательным, но в ряде случаев является полезным: может повыситься наглядность кода, появляется возможность указать спецификатор доступа protected (см. раздел 1.3).
Но более существенная польза будет в ситуациях, когда из-за нарушения дополнительных условий из раздела 2.2 компилятор не будет генерировать функцию-член. В этом случае (если, конечно, программиста устраивает функция-член, сгенерированная компилятором) такое объявление освобождает его от написания рутинного кода и гарантирует отсутствие ошибок.
Рассмотрим примеры.
class Point
{
int X{}, Y{};
public:
Point(int x, int y) : X(x), Y(y) {} // конструктор
// с параметрами
Point() = default; // конструктор по умолчанию
Point(const Point&) = default; // копирующий конструктор
Point& operator=(const Point&) = default; // оператор
// копирующего присваивания
// ...
};
В этом классе объявлен конструктор с параметрами, поэтому конструктор по умолчанию не будет генерироваться компилятором без его объявления со спецификатором =default. Если такого объявления не сделать, то в случаях, когда конструктор по умолчанию нужен, компилятор выдаст ошибку. Объявления копирующего конструктора и оператора копирующего присваивания в данном примере не обязательны, если их не будет, то компилятор при необходимости сгенерирует эти функции-члены самостоятельно.
Вот другой пример.
class Dictionary
{
std::map<int, std::string> m_Dict;
public:
~Dictionary() { std::cout << "Dictionary, dtor\n"; }
Dictionary(Dictionary&&) = default; // перемещающий
// конструктор
Dictionary& operator=(Dictionary&&) = default; // оператор
// перемещающего присваивания
// ...
};
Конструктор по умолчанию, копирующий конструктор и оператор копирующего присваивания не объявлены, их сгенерирует компилятор. Мы явно определили деструктор, так как хотим трассировать уничтожение объекта. Перемещающий конструктор и оператор перемещающего присваивания, сгенерированные компилятором, нас бы устроили (std::map<> поддерживает перемещение), но в классе объявлен деструктор, а это нарушение дополнительных условий из раздела 2.2 и поэтому компилятор не будет их генерировать. В этом случае мы объявляем эти функции-члены со спецификатором =default, то есть «просим» компилятор сгенерировать их реализации. Если мы этого не сделаем, то класс Dictionary не будет поддерживать перемещение и для экземпляров класса будет выполняться копирование, там, где могло бы быть выполнено перемещение. А это может привести в ряде случаев к ощутимому снижению эффективности, причем все это будет происходить «молча», без всяких предупреждений.
Спецификатор =default можно не задавать при объявлении, а использовать его при определении специальной функции-члена. Вот пример:
class Point
{
int X{}, Y{};
public:
Point(int x, int y) : X(x), Y(y) {} // конструктор
// с параметрами
Point(); // конструктор по умолчанию
// ...
};
Point::Point() = default; // конструктор по умолчанию
Такое решение может иногда потребоваться для изменения контекста генерации функции-члена, см. [Meyers], раздел 4.5.
## 2.4. Алгоритм генерации компилятором специальных функций-членов Реализация специальной функции-члена, генерируемая компилятором, должна обеспечивать вызовы той же функции-члена для всех членов и базовых частей. Если программист не определил нужную специальную функцию-член для типа члена или базового класса, то компилятор будет пытаться генерировать ее самостоятельно.
Порядок вызова специальных функций-членов для членов и базовых частей следующий: для конструкторов и операторов присваивания — от самых дальних базовых классов к непосредственным базовым классам (при множественном наследовании в порядке объявления) и далее члены класса в порядке объявления. Для деструкторов порядок в точности обратный.
Успешное завершение описанной процедуры не гарантируется, результат будет зависеть от некоторых дополнительных условий. Рассмотрим эти условия.
Нужная функция-член не объявлена удаленной.
Если нужная функция-член определена программистом или объявлена со спецификатором
=default, то она должна быть доступна (иметь спецификатор доступаpublicдля типа члена,public/protectedдля базового класса или находится в дружественном контексте).Если нужная функция-член объявлена программистом без спецификатора
=default, то она должна быть определена программистом, иначе компоновщик выдаст ошибку.Если нужная функция-член не объявлена, то для ее генерации компилятором должны выполняться дополнительные условия из раздела 2.2.
Если в описанной процедуре генерации функции-члена эти условия нарушаются на каком-то шаге, то в случае перемещающей операции будет попытка ее замены на соответствующую копирующую, а в остальных случаях попытка генерации потерпит неудачу.
Приведем пример действия описанных правил. Для того, чтобы запретить копирование, перемещение и присваивание экземпляров класса нужно запретить генерацию копирующего и перемещающего конструктора и соответствующих операторов присваивания (и, естественно, не определять эти функции-члены). Для этого можно использовать следующий класс в качестве базового:
class NonCopyable
{
protected:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};
Мы объявляли копирующий конструктор и оператор копирующего присваивания класса NonCopyable удаленными, поэтому компилятор не будет их генерировать, соответственно, запрет на генерацию этих функций-членов будет распространяться на любой класс, имеющий класс NonCopyable базовым в какой-нибудь цепочке наследования (наследование может быть закрытым, private).
Компилятор не будет генерировать перемещающий конструктор и оператор перемещающего присваивания класса NonCopyable из-за нарушения дополнительных условий из раздела 2.2 (в классе объявлены копирующий конструктор и оператор копирующего присваивания). Заменить перемещающие операции копирующими аналогами нельзя, так как копирующие операции объявлены удаленными. Соответственно, запрет на генерацию этих функций-членов будет распространяться на любой производный класс.
Мы объявили конструктор по умолчанию со спецификатором =default, так как без этого объявления компилятор не будет его генерировать из-за объявления копирующего конструктора (опять нарушение дополнительных условий из раздела 2.2) и, соответственно, компилятор не будет генерировать конструктор по умолчанию в любом производном классе, а это совсем не нужное ограничение. Более того, в производном классе вообще нельзя будет определить какой-нибудь конструктор, см. раздел 3.1.
Обратим внимание на то, что использование класса NonCopyable в качестве базового не увеличивает размер экземпляра производного класса, так как компилятор в этом случае будет применять оптимизацию пустого базового класса (empty base optimization, EBO).
# 3. Конструкторы и деструктор Если функция-член определена программистом, то в случае конструктора и деструктора компилятор также принимает участие в ее реализации и может сгенерировать часть кода. Рассмотрим эту ситуацию подробнее.
## 3.1. Инициализация и тривиальные типы
### 3.1.1. Общая схема инициализации Для конструкторов компилятор обеспечивает инициализацию для всех членов и базовых частей. Конструктор может быть как определен программистом, так и сгенерирован компилятором. Порядок обход такой же, как и для конструкторов, сгенерированных компилятором, и те же требования по доступу к нужным конструкторам в соответствии с правилами раздела 2.4. Код тела конструктора будет выполняться в последнюю очередь.
Для членов и базовых частей могут быть заданы инициализаторы (как это сделать будет описано в разделах 3.1.6 и 3.1.7). При наличии инициализатора компилятор будет использовать конструктор, подходящий по параметрам аргументам инициализатора, либо будет использоваться агрегатная инициализация (см. раздел 3.1.8). Для инициализаторов с пустым списком аргументов будет использован конструктор по умолчанию. Если он не определен программистом, то будет сгенерирован компилятором.
Для членов и базовых частей, для которых не задан инициализатор, компилятор будет использовать конструктор по умолчанию. Если он не определен программистом, то будет сгенерирован компилятором.
Таким образов, конструктор является контекстом, который может потребовать использование конструктора по умолчании для членов и базовых частей. Но правила генерации конструктора по умолчанию требуют уточнений, которые связаны с так называемыми тривиальными типами. Далее в разделе 3.1.2 рассмотрим тривиальные типы и в разделе 3.1.4 правила генерации конструктора по умолчанию.
### 3.1.2. Тривиальные типы Тривиальные типы (trivial type) были введены для решения проблем совместимости с C. К ним относятся числовые типы, тип `bool`, перечисления, типы указателей, а также массивы, классы, структуры, объединения, состоящие из тривиальных типов. Классы, структуры и объединен��я должны удовлетворять некоторым дополнительным условиям: отсутствие определяемых программистом специальных функций-членов, удаленных специальных функций-членов, а также виртуальных функций-членов. Без нарушения условий тривиальности можно объявлять специальные функции-члены со спецификатором `=default`, определять обычные функции-члены, статические члены и функции-члены, а также конструкторы с параметрами.
В стандартной библиотеке С++11 есть шаблоны, называемые свойствами типов (заголовочный файл <type_traits>). Один из них позволяет определить, является ли тип тривиальным. Выражение std::is_trivial<Т>::value будет иметь значение true, если T тривиальный тип и false в противном случае.
Главная проблема, связанная с тривиальными типами, состоит в том, что в процессе инициализации данные тривиального типа могут оказаться неинициализированными, то есть получить в качестве значения случайный набор битов, который имела выделенная под объект память. Для тривиальных типов также возможна нулевая инициализация, когда все данные получают нулевое значение соответствующего типа. Разберемся, когда происходит нулевая инициализация, а когда данные тривиального типа оказываются неинициализированными.
Отметим, что неинициализированные переменные иногда выявляет компилятор и выдает при этом предупреждение или даже ошибку. Еще лучше неинициализированные переменные выявляют статические анализаторы кода.
### 3.1.3. Статическая память Если переменная объявленных глобально, или в области видимости пространства имен, или является статическим членом класса, то неинициализированные тривиальные данные будут побитово обнулены. По этой причине дальнейшее изложение будет относиться к локальным переменным и массивам, а также к динамическим объектам и массивам.
### 3.1.4. Конструктор по умолчанию, сгенерированный компилятором Тривиальные типы являются причиной того, что компилятор генерирует два варианта конструктор по умолчанию. Первый, без инициализации, генерируется в следующих случаях: 1. При объявлении переменной без инициализатора; 2. Для членов и базовых частей без инициализатора в контексте конструктора без инициализации; 3. Для членов и базовых частей класса без инициализатора в контексте конструктора, определенного программистом.
В таком конструкторе члены и базовые части тривиального типа без инициализатора будут неинициализированными.
Второй, с инициализацией, генерируется в следующих случаях:
При объявлении переменной с инициализатором;
Для членов и базовых частей с инициализатором;
Для членов и базовых частей класса без инициализатора в контексте конструктора с инициализацией (то есть в этом случае действие инициализатора будет распространяться дальше по графу объекта).
В таком конструкторе члены и базовые части тривиального типа без инициализатора получат нулевую инициализацию.
Пусть тип T не имеет конструктора, определенного программистом. Вот пример объявлений, когда для T будет генерироваться конструктор без инициализации.
T x;
T a[N];
T* p = new T;
T* q = new T[N];
Вот пример объявлений, когда для T будет генерироваться конструктор с инициализацией.
T x{};
T a[N]{};
T* p = new T{};
T* q = new T[N]{};
### 3.1.5. Конструктор, определенный программистом Как вытекает из описания предыдущего раздела, в таком конструкторе для членов и базовых части без инициализатора компилятор будет генерировать конструктор по умолчанию без инициализации.
Обратим внимание на то, что в конструкторе, определенном программистом, имеется дополнительная возможность задать значения для членов и базовых частей тривиального типа: это можно сделать в теле конструктора, например, с помощью присваивания.
### 3.1.6. Список инициализации конструктора Инициализаторы для членов или базовых частей можно задавать с помощью списка инициализации конструктора. Вот пример: ```cpp class B { protected: B(int x); // ... };
class G : public B
{
std::string m_M1;
std::string m_M2;
public:
G(int b, const char* m1)
: B{b}, m_M1{m1}, m_M2{} // список инициализации конструктора
{ /* ... */} // тело конструктора
// ...
};
Важным правилом, связанным со списком инициализации конструктора, является порядок выполнения инициализации. Этот порядок определяется не порядком элементов в списке, а порядком, в котором компилятор осуществляет обход членов и базовых частей при инициализации (см. раздел 3.1.1). Порядок в списке инициализации конструктора не имеет значения. Это правило может оказаться неожиданностью для программиста и стать источником ошибки, если элементы списка инициализации ссылаются на другие элементы из этого же списка.
<anchor>id-3-1-7</anchor>
### 3.1.7. Инициализатор при объявлении члена
В C++11 появилась дополнительная возможность задать инициализатор для нестатических членов при объявлении класса или структуры. В этом случае тип будет нетривиальным, инициализация будет выполнена в любом конструкторе, как определенном программистом, так и сгенерированном компилятором. Таким образом, тривиальные члены с инициализатором гарантированно будут инициализированы. Порядок инициализации определяется порядком объявления членов. Настоятельно рекомендуется использовать эту возможность при проектировании типов. Рассмотрим пример:
```cpp
struct Point
{
int X, Y;
Point() = default;
};
struct Point2
{
int X{}, Y{};
Point2() = default;
};
Тип Point является тривиальным, при объявлении локальной переменной
Point p;
переменная p окажется неинициализированной, члены p.X и p.Y будут иметь случайные значения.
Тип Point2 является нетривиальным, при объявлении локальной переменной
Point2 p2;
члены p2.X и p2.Y будут иметь нулевые значения.
### 3.1.8. Непустой инициализатор Рассмотрим теперь особенности инициализатор с аргументами. ```cpp T x{param_list}; T a[N]{param_list}; T* p = new T{param_list}; T* q = new T[N]{param_list}; ``` Здесь могут быть два варианта.
В первом из них тип T является агрегатным (aggregate type) или объявлен массив. (Требования для агрегатного типа: все его нестатические члены должны быть общедоступными, не объявлены конструкторы и виртуальные функции-члены, нет базовых классов. Есть свойство типа std::is_aggregate<>, которое позволяет определить является ли тип агрегатным.) В этом случае будет использована унаследованная из C агрегатная инициализация. Элементы param_list должны быть корректными инициализаторами для членов T в порядке объявления или элементов массива. Важная особенность агрегатной инициализации заключается в том, что число элементов param_list может быть меньше, чем число членов класса или элементов массива. В этом случае члены класса или элементы массива, которым не хватило элементов из param_list, будут инициализированы с помощью конструктора по умолчанию с инициализацией.
Во втором варианте тип T не является агрегатным и не объявлен массив. В этом случае тип T должен иметь конструктор, который принимает param_list, инициализация будет выполнена по правилам раздела 3.1.5.
## 3.2. Деструктор Реализация деструктора, как определенная программистом, так и сгенерированная компилятором, должна обеспечивать вызовы деструкторов для всех членов и базовых частей. Эта функциональность полностью обеспечивается компилятором. Если деструктор определен программистом, то в его коде нельзя вставлять вызовы деструкторов для членов и базовых частей. Можно считать, что после выполнения тела деструктора всегда вызывается деструктор, сгенерированный компилятором. Деструктор является ключевым элементом в стратегии управления ресурсами, и такое решение гарантирует отсутствие утечек ресурса. Соответственно, все правила успешной генерации специальной функции-члена, рассмотренные в разделе 2.4 должны соблюдаться для деструктора всегда.
Например, если проектируется полиморфная иерархия классов, то в базовом классе виртуальные функции часто объявляются как чисто виртуальные, для этого используется спецификатор =0. Такие функции должны быть переопределены в производных классах. Деструктор также надо объявить виртуальным, но не чисто виртуальным, а со спецификатором =default, так как для чисто виртуального деструктора в соответствии с правилами раздела 2.4 придется делать определение. С++ разрешает определять чисто виртуальные функции, но в случае деструктора это, как правило, совершенно не нужная работа, которая, кроме того, может создать дополнительные проблемы, связанные с компоновкой.
# 4. Специальные функции-члены, определяемые программистом В ряде случаев специальные функции-члены, генерируемые компилятором, не годятся, программист должен определить их сам. В качестве примера можно привести классы, владеющие ресурсами. В типичном варианте такой класс имеет член, называемый сырым дескриптором ресурса (raw resource handle). Чаще всего он имеет тип указателя, но могут быть и другие варианты. Через этот дескриптор, класс получает доступ к ресурсу. Класс, владеющий ресурсом, в какой-то момент обязан его освободить и для этого также используется дескриптор. Обычно это делается в деструкторе с помощью кода, зависящего от типа ресурса, автоматически освободить ресурс через сырой дескриптор нельзя (на то он и назван сырым).
Кроме деструктора, программист должен определить копирующий конструктор и оператор копирующего присваивания. Если использовать копирующий конструктор, сгенерированный компилятором, то при копировании экземпляра класса мы получим две копии дескриптора одного и того же ресурса. Если один объект освободит ресурс, то другой будет иметь возможность доступа к удаленному ресурсу или повторного удаления уже удаленного ресурса, что часто заканчивается неопределенным поведением (undefined behavior, UB). Оператор присваивания, сгенерированный компилятором, также не годится, так как он просто перезапишет дескриптор целевого объекта без освобождения ресурса, что приведет к утечке ресурса. Здесь действует известное «правило большой тройки», которое утверждает, что если программист определил хотя бы одну из трех операций — копирующий конструктор, оператор копирующего присваивания или деструктор, — то он должен определить все три операции. Подробнее об этом можно почитать у Скотта Мейерса [Meyers].
Для класса, владеющего ресурсом, определения копирующих операций можно сделать разными способами, они зависят от типа ресурса и планируемого поведения ресурса при копировании. Некоторые из этих вариантов могут быть весьма ресурсоемкими. Но есть два относительно простых варианта. В первом варианте мы просто запрещаем копирование и перемещение, получая так называемый non-copyable класс, экземпляры которого нельзя ни копировать, ни перемещать. Во втором варианте запрещаем копирование и определяем перемещающие операции, в которых выполняется перемещение дескриптора. В этом случае мы получим так называемый move-only класс, экземпляры которого нельзя копировать, но можно перемещать. В обоих случаях дескриптор всегда будет в единственном экземпляре и описанной выше проблемы не возникнет. Подробнее тема управления ресурсами в C++ изложена в другой статье автора.
Для запрета копирования и присваивания экземпляров класса надо объявить копирующий конструктор и оператор копирующего присваивания удаленными, то есть со спецификатором =delete (см. раздел 2.1). Если при этом не объявлять перемещающий конструктор и оператор перемещающего присваивания, то перемещение экземпляров класса также будет запрещено и получим non-copyable класс, а если определить эти функции-члены надлежащим образом, то получим move-only класс.
Экземпляры move-only классов можно хранить в стандартных контейнерах практически без ограничений, также нет ограничений на использование стандартных алгоритмов. В стандартной библиотеке довольно много move-only классов, например, std::unique_ptr, std::thread, std::fstream и другие классы ввода-вывода. Еще одно достоинство move-only классов заключается в том, что перемещение никогда не будет заменено на копирование.
В качестве примера рассмотрим простой move-only класс, владеющий буфером памяти.
class SBuff
{
using Byte = std::uint8_t;
Byte* m_Buff{}; // сырой дескриптор ресурса
public:
// захват ресурса
SBuff(std::size_t size) : m_Buff(new Byte[size]) {}
// освобождение ресурса
~SBuff() { delete[] m_Buff; }
// запрет копирования
SBuff(const SBuff&) = delete;
SBuff& operator=(const SBuff&) = delete;
// перемещение
SBuff(SBuff&& src) noexcept;
SBuff& operator=(SBuff&& src) noexcept;
// ...
};
// определение перемещения
SBuff::SBuff(SBuff&& src) noexcept
: m_Buff(src.m_Buff)
{
src.m_Buff = nullptr;
}
SBuff& SBuff::operator=(SBuff&& src) noexcept
{
SBuff tmp{ std::move(src) };
std::swap(m_Buff, tmp.m_Buff);
return *this;
}
Оператор присваивания освобождает ресурс, которым перед присваиванием владеет целевой объект, это обеспечивает известная идиома «копирование и обмен».
Иногда можно избежать ручного управления ресурсом. Это происходит, когда удается агрегировать сырой дескриптор ресурса в подходящий класс. В этом случае всю необходимую работу по управлению ресурсом будут выполнять специальные функции-члены, сгенерированные компилятором. Вот как можно переписать предыдущий пример, используя специализацию для массивов шаблона std::unique_ptr<>.
class SBuff
{
using Byte = std::uint8_t;
std::unique_ptr<Byte[]> m_Buff;
public:
SBuff(std::size_t size)
: m_Buff(std::make_unique<Byte[]>(size)){}
};
Мы получили move-only класс функционально идентичный предыдущему варианту. Все специальные функции-члены, кроме конструктора по умолчанию, при необходимости будут сгенерированы компилятором.
# 5. Объединения В объединениях (ключевое слово `union`) все члены имеют одинаковое нулевое смещение, то есть накладываются друг на друга. При изменении одного члена почти всегда меняются значения всех других членов. В C++98 объединения перешли из C без всяких изменений, поэтому все члены объединения должны были иметь тривиальный тип, нельзя было задавать инициализаторы, определять функции-члены, задавать спецификаторы доступа и использовать наследование. Ситуация изменилась в C++11: теперь члены объединения могут иметь нетривиальный тип, разрешена инициализация, можно определять функции-члены, в том числе и специальные, задавать спецификаторы доступа. Статические члены, виртуальные функции-члены и наследование остались под запретом. Но использование этих новых возможностей имеет некоторую специфику, отличную от классов и структур. Рассмотрим эти особенности подробнее.
## 5.1. Инициализация В случае инициализации необходимы правила выбора — какой именно член будет инициализирован. Здесь есть два варианта.
Если в объединении не определены конструкторы, то в этом случае инициализатор будет применен к первому члену. Соответственно, инициализатор должен подходить типу первого члена. Вот пример:
union U1
{
double V;
struct { int X, Y; } P;
};
U1 u1{ {3.14} }; // V получит значение 3.14
U1 u2{}; // V получит значение 0.0
U1 u3{ "meow" }; // ошибка, неподходящий инициализатор для V
U1 u4; // u4 будет неинициализированной, так как
// U1 является тривиальным типом
Если объединение имеет нетривиальные члены, то нельзя объявлять переменные без инициализатора.
Если в объединении определены конструкторы, то в этом случае в них должно быть явно указано, какой член инициализируются, причем в списке инициализации можно указать только один член. Вот пример:
union U2
{
double V;
struct { int X, Y; } P;
U2(double v) : V{v}{} // конструктор для V
U2(int x, int y) : P{x, y}{} // конструктор для P
};
U2 u5{3.14}; // V получит значение 3.14
U2 u6{1, 2}; // P.X получит значение 1, P.Y получит значение 2
U2 u7{"meow"}; // ошибка, нет подходящего конструктора
U2 u8{}; // ошибка, нет конструктора по умолчанию
Конструктор по умолчанию со спецификатором =default, допустим, только если типы членов не имеют конструкторов, определенных программистом.
## 5.2. Копирующий конструктор, оператор копирующего присваивания, деструктор Если в объединении есть член, тип которого имеет операцию из «большой тройки», определенную программистом, то для ее поддержки в объединении программист должен такую операцию определить явно, иначе она будет считаться удаленной. В этом определении можно вызвать соответствующую операцию для какого-нибудь члена, а можно оставить его пустым. Вот пример: ```cpp union U3 { double X; std::string S; U3& operator=(const U3& s) { S = s.S; return *this; } }; ``` Если вызывается деструктор для члена, то надо быть уверенным, что инициализирован именно он, для копирующего конструктора надо быть уверенным, что инициализирован соответствующий член аргумента, а для оператора копирующего присваивания эти требования объединяются. Такую информацию само объединение не дает, а при неправильном выборе возможно UB. Более реалистичный вариант возлагает на внешний код ответственность за информацию об инициализированном члене, соответственно, он и должен вызывать операцию для этого члена, а определение функции-члена для объединения становится вообще не нужным.
## 5.3. Выводы Новые возможности языка для объединений вызывают неоднозначную оценку. Правила инициализации довольно разумны, член для инициализации выбирается на основе аргументов инициализатора. Но вот, что касается операций «большой тройки», то в этом случае для их корректной работы надо знать, какой член инициализирован (иначе возможно UB), а такую информацию само объединение не дает, так что реализация этих операций должна быть во внешнем коде, который хранит информацию об инициализированном члене. В C++17 появился шаблон `std::variant<>`, который решает все эти проблемы.
# 6. Список статей серии «C++, копаем вглубь» 1. [Перегрузка в C++. Часть I. Перегрузка функций и шаблонов.](https://habr.com/ru/articles/487920/) 2. [Перегрузка в C++. Часть II. Перегрузка операторов.](https://habr.com/ru/articles/489666/) 3. [Перегрузка в C++. Часть III. Перегрузка операторов new/delete.](https://habr.com/ru/articles/490640/) 4. [Массивы в C++.](https://habr.com/ru/articles/495444/) 5. [Ссылки и ссылочные типы в C++.](https://habr.com/ru/articles/646005/) 6. [Объявление и инициализация переменных в C++.](https://habr.com/ru/articles/790010/) 7. [Константность в C++.](https://habr.com/ru/articles/796233/) 8. [Использование неполных объявлений в C++.](https://habr.com/ru/articles/889808/)
# 7. Итоги Программист должен знать при каких условиях и каким образом компилятор генерирует специальные функции-члены. Использование этой возможности делает код более лаконичным и надежным.
При объявлении нестатических членов класса или структуры рекомендуется задавать инициализатор, это гарантирует их инициализацию в любом конструкторе.
Если специальная функция-член может быть сгенерирована компилятором и сгенерированная версия устраивает программиста, то встает вопрос: либо не объявлять ее совсем, либо объявить ее со спецификатором =default. Ответ может быть разный в зависимости от функции-члена.
Для конструктора по умолчанию рекомендуется сделать такое объявление. Это объявление не влияет на генерацию компилятором других функций-членов. При этом можно использовать спецификатор доступа protected, что полезно для классов, которые можно использовать только как базовые. Кроме того, класс без конструктора выглядит каким-то недоделанным.
Что касается членов «большой тройки» — копирующего конструктора, оператора копирующего присваивания, деструктора, — то их лучше не объявлять, так как особой пользы от этого нет, кроме того такое объявление блокирует генерацию перемещающих функций-членов. Исключением является базовый класс полиморфной иерархии, в нем деструктор надо объявить виртуальным со спецификатором =default.
Особое внимание нужно уделять перемещению (если оно, конечно, актуально для класса). Условия генерации перемещающих функций-членов довольно запутанные и легко могут поменяться в процессе разработки. Если компилятор не сгенерирует перемещающие функции-члены, то перемещение будет «молча» заменено на копирование, что может привести к снижению эффективности. Поэтому при возникновении сомнений, их лучше объявить. Если при этом копирующие функции-члены объявить удаленными, то получится move-only класс.
Если в классе требуется ручное управление ресурсом, то рекомендуется создать для этого отдельный минимальный класс. В деструкторе такого класса надо освобождать ресурс. Также в этом классе надо определить (либо объявить удаленными) специальные функции-члены, реализующие копирующие и перемещающие операции в соответствии с требуемым поведением ресурса при этих операциях. Такой класс можно агрегировать в другой класс или использовать в качестве базового класса. В этом случае писать код для управления ресурсом уже будет не нужно: всю необходимую работу будут выполнять специальные функции-члены, генерируемые компилятором.
# Список литературы [Dewhurst] Дьюхэрст, Стефан К. Скользкие места C++. Как избежать проблем при проектировании и компиляции ваших программ.: Пер. с англ. — М.: ДМК Пресс, 2012.
[Meyers]
Мейерс, Скотт. Эффективный и современный C++: 42 рекомендации по использованию C++11 и C++14.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2016.