А.3. Умалчиваемые функции

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

Зачем это может понадобиться? Есть несколько причин.

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

• Для документирования. Если сгенерированной компилятором версии достаточно, то имеет смысл так прямо и сказать. Тогда всякий, кто впоследствии будет читать код, поймёт, что это сделано намеренно.

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

• Чтобы сделать деструктор виртуальным и при этом генерируемым компилятором.

• Чтобы сгенерировать специальный вариант копирующего конструктора, например, принимающий параметр по неконстантной ссылке (по умолчанию генерируется конструктор, принимающий константную ссылку).

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

Умалчиваемые функции объявляются путем добавления спецификатора = default, например:

class Y {

private:

 Y() = default; ← Изменяем видимость

public:

 Y(Y&) = default; ← Принимаем не-const ссылку

 T& operator=(const Y&) = default;←┐ объявляем умалчиваемой

                                   │ для документирования

protected:

 virtual ~Y() = default; ← Изменяем видимость и добавляем virtual

};

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

• Объекты с тривиальными копирующим конструктором, копирующим оператором присваивания и деструктором можно копировать с помощью memcpy или memmove.

• Литеральные типы, используемые в constexpr-функциях (см. раздел А.4) обязаны обладать тривиальными конструктором, копирующим конструктором и деструктором.

• Классы с тривиальными конструктором по умолчанию, копирующим конструктором, копирующим оператором присваивания и деструктором можно использовать в объединении (union), в котором определены пользовательские конструктор и деструктор.

• Классы с тривиальными конструктором копирующим оператором присваивания можно использовать вместе с шаблонным классом std::atomic<> (см. раздел 5.2.6), то есть передавать значения такого типа атомарным операциям.

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

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

struct aggregate {

 aggregate() = default;

 aggregate(aggregate const&) = default;

 int a;

 double b;

};

aggregate x={42, 3.141};

В данном случае x.a инициализируется значением 42, a x.b — значением 3.141.

Третье различие малоизвестно и относится только к конструктору по умолчанию, да и то лишь в классах, удовлетворяющих определенному условию. Рассмотрим такой класс:

struct X {

 int а;

};

Если экземпляр класса X создается без инициализатора, то содержащееся в нем значение (а) типа int инициализируется по умолчанию. Если у объекта статический класс памяти, то значение инициализируется нулем, в противном случае начальное значение произвольно, что может привести к неопределённому поведению, если программа обращается к объекту раньше, чем ему будет присвоено значение:

X x1; ← значение x1.a не определено

С другой стороны, если инициализировать экземпляр X путем явного вызова конструктора по умолчанию, то он получит значение 0:

X x2 = X(); ← x2.а == 0

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

У этого замысловатого и потенциально чреватого ошибками правила есть тем не менее применения, а, если вы пишете конструктор по умолчанию самостоятельно, то это свойство утрачивается; данные-члены (например, а) либо всегда инициализируются (коль скоро вы указали значение или явно вызвали конструктор по умолчанию), либо вообще не инициализируются (если вы этого не сделали):

X::X() : а() {}   ← всегда а == 0

X::X() : а(42) {} ← всегда а == 42

X::X() {}         ← (1)

Если инициализация а при конструировании X не производится (как в третьем примере (1)), то a остается неинициализированным для нестатических экземпляров X и инициализируется нулем для экземпляров X со статическим временем жизни.

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

X::X() = default;

Это свойство используется в атомарных типах (см. раздел 5.2), в которых конструктор по умолчанию явно объявлен умалчиваемым. У таких типов начальное значение не определено, если только не выполняется одно из следующих условий: (а) задан статический класс памяти (тогда значение инициализируется нулем); (b) для инициализации нулем явно вызван конструктор по умолчанию; (с) вы сами явно указали начальное значение. Отметим, что в атомарных типах конструктор для инициализации значением объявлен как constexpr (см. раздел А.4), чтобы разрешить статическую инициализацию.