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() : а() {} ←
всегда а == 0X::X() : а(42) {} ←
всегда а == 42X::X() {} ←
(1)Если инициализация
а
при конструировании X
не производится (как в третьем примере (1)), то a
остается неинициализированным для нестатических экземпляров X
и инициализируется нулем для экземпляров X
со статическим временем жизни.Обычно, если вы вручную напишете хотя бы один конструктор, то компилятор не станет генерировать конструктор по умолчанию. Стало быть, если он вам все-таки нужен, его придётся написать самостоятельно, а тогда это странное свойство инициализации теряется. Однако явно объявив конструктор умалчиваемым, вы можете заставить компилятор сгенерировать конструктор по умолчанию и сохранить это свойство:
X::X() = default;
Это свойство используется в атомарных типах (см. раздел 5.2), в которых конструктор по умолчанию явно объявлен умалчиваемым. У таких типов начальное значение не определено, если только не выполняется одно из следующих условий: (а) задан статический класс памяти (тогда значение инициализируется нулем); (b) для инициализации нулем явно вызван конструктор по умолчанию; (с) вы сами явно указали начальное значение. Отметим, что в атомарных типах конструктор для инициализации значением объявлен как
constexpr
(см. раздел А.4), чтобы разрешить статическую инициализацию.А.4. constexpr
-функции
Целые литералы, например
42
, — это константные выражения. Равно как и простые арифметические выражения, например 23*2-4
. Частью константного выражения могут быть также const
-переменные любого целочисленного типа, которые сами инициализированы константным выражением:const int i = 23;
const int two_i = i * 2;
const int four = 4;
const int forty_two = two_i - four;
Помимо использования константных выражений для инициализации переменных, которые могут использоваться в других константных выражениях, есть ряд случаев, где разрешается применять только константные выражения.
• Задание границ массива:
int bounds = 99; │
Ошибка, bounds — не константноеint array[bounds];←┘
выражениеconst int bounds2 = 99;│
Правильно, bounds2 — константноеint array2[bounds2]; ←┘
выражение• Задание значения параметра шаблона, не являющего типом:
template
struct test {}; │
Ошибка, bounds —│
не константноеtest is;←┘
выражениеtest ia2;←┐
Правильно, bounds2 —│
константное выражение• Задание непосредственно в определении класса инициализатора для переменной-члена класса целочисленного типа со спецификаторами
static const
:class X {
static const int the_answer = forty_two;
};
• Употребление в инициализаторах встроенных типов или агрегатов, применяемых для статической инициализации:
struct my_aggregate {
int a;
int b;
};
static my_aggregate ma1 =│
Статическая { forty_two, 123 }; ←┘
инициализацияint dummy = 257; │
Динамическаяstatic my_aggregate ma2 = {dummy, dummy};←┘
инициализацияТакая статическая инициализация полезна для предотвращения зависимости от порядка инициализации и состояний гонки.
Всё это не ново и было описано еще в стандарте С++ 1998 года. Но в новом стандарте появилось и дополнение в части константных выражений — ключевое слово
constexpr
.Ключевое слово
constexpr
применяется главным образом как модификатор функции. Если параметр и возвращаемое функцией значение удовлетворяют определенным условиям, а тело функции достаточно простое, то в ее объявлении можно указать constexpr
и использовать функцию в константных выражениях. Например:constexpr int square(int x) {
return x*x;
}
int array[square(5)];
В этом случае массив
array
будет содержать 25 значений, потому что функция square
объявлена как constexpr
. Конечно, из того, что функцию можно использовать в константном выражении, еще не следует, что любой случай ее использования автоматически будет константным выражением:int dummy = 4;
(1) Ошибка, dummy — не константноеint array[square(dummy)];←┘
выражениеВ этом примере
dummy
не является константным выражением (1), поэтому не является таковым и square(dummy)
. Это обычный вызов функции, и, следовательно, для задания границ массива array его использовать нельзя.А.4.1. constexpr
и определенные пользователем типы
До сих пор мы употребляли в примерах только встроенные типы — такие, как
int
. Но в новом стандарте С++ допускаются константные выражения любого типа, удовлетворяющего требованиям, предъявляемым к литеральному типу. Чтобы тип класса можно было считать литеральным, должны быть выполнены все следующие условия:• в классе должен существовать тривиальный копирующий конструктор;
• в классе должен существовать тривиальный деструктор;
• все нестатические переменные-члены данного класса и его базовых классов должны иметь тривиальный тип;
• в классе должен существовать либо тривиальный конструктор по умолчанию, либо
constexpr
-конструктор, отличный от копирующего конструктора.О
constexpr
-конструкторах мы поговорим чуть ниже. А пока обратимся к классам с тривиальным конструктором по умолчанию. Пример такого класса приведён ниже:class CX {
private:
int а;
int b;
public:
CX() = default; ←
(1) CX(int a_, int b_) : ←
(2) a(a_), b(b_) {}
int get_a() const {
return a;
}
int get_b() const {
return b;
}
int foo() const {
return a + b;
}
};
Здесь мы явно объявили конструктор по умолчанию (1)умалчиваемым (см. раздел А.3), чтобы сохранить его тривиальность, несмотря на наличие определённого пользователем конструктора (2). Таким образом, этот тип удовлетворяет всем требованиям к литеральному типу и, значит, его можно использовать в константных выражениях. К примеру, можно написать
constexpr
-функцию, которая создает новые экземпляры этого класса:constexpr CX create_cx() {
return CX();
}
Можно также написать простую
constexpr
-функцию, которая копирует свой параметр:constexpr CX clone(CX val) {
return val;
}
Но это практически и всё, что можно сделать, —
constexpr
-функции разрешено вызывать только другие constexpr
-функции. Тем не менее, допускается применять спецификатор constexpr
к функциям-членам и конструкторам CX:class CX {
private:
int а;
int b;
public:
CX() = default;
constexpr CX(int a_, int b_): a(a_), b(b_) {}
constexpr int get_a() const { ←
(1) return a;
}
constexpr int get_b() { ←
(2) return b;
}
constexpr int foo() {
return a + b;
}
};
Отметим, что теперь квалификатор
const
в функции get_a()
(1) избыточен, потому что он и так подразумевается ключевым словом constexpr
. Функция get_b()
достаточно «константная» несмотря на то, что квалификатор const
опущен (2). Это дает возможность строить более сложные constexpr
-функции, например:constexpr CX make_cx(int a) {
return CX(a, 1);
}
constexpr CX half_double(CX old) {
return CX(old.get_a()/2, old.get_b()*2);
}
constexpr int foo_squared(CX val) {
return square(val.foo());
}
int array[foo_squared(
half_double(make_cx(10)))]; ←
49 элементовВсё это, конечно, интересно, но уж слишком много усилий для того, чтобы всего лишь вычислить границы массива или значение целочисленной константы. Основное же достоинство константных выражений и
constexpr
-функций в контексте пользовательских типов заключается в том, что объекты литерального типа, инициализированные константным выражением, инициализируются статически и, следовательно, не страдают от проблем, связанных с зависимостью от порядка инициализации и гонок.CX si = half_double(CX(42, 19));
Это относится и к конструкторам. Если конструктор объявлен как
constexpr
, а его параметры — константные выражения, то такая инициализация считается константной инициализацией и происходит на этапе статической инициализации. Это одно из наиболее важных изменений в стандарте C++11 с точки зрения параллелизма: разрешив статическую инициализацию для определенных пользователем конструкторов, мы предотвращаем состояния гонки во время инициализации, поскольку объекты гарантированно инициализируются до начала выполнения программы.Особенно существенно это для таких классов, как
std::mutex
(см. раздел 3.2.1) и std::atomic<>
(см. раздел 5.2.6), поскольку иногда мы хотим, чтобы некий глобальный объект синхронизировал доступ к другим переменным, но так, чтобы не было гонок при доступе к нему самому. Это было бы невозможно, если бы конструктор мьютекса мог стать жертвой гонки, поэтому конструктор по умолчанию в классе std::mutex
объявлен как constexpr
, чтобы инициализация мьютекса всегда производилась на этапе статической инициализации.А.4.2. constexpr
-объекты
До сих пор мы говорили о применении
constexpr
к функциям. Но этот спецификатор можно применять и к объектам. Чаще всего, так делают для диагностики; компилятор проверяет, что объект инициализирован константным выражением, constexpr
-конструктором или агрегатным инициализатором, составленным из константных выражений. Кроме того, объект автоматически объявляется как const
:constexpr int i = 45;←
Правильноconstexpr std::string s("hello");←┐
Ошибка, std::string —int foo(); │
не литеральный типconstexpr int j = foo();←
Ошибка, foo() не объявлена как constexprA.4.3. Требования к constexpr
-функциям
Чтобы функцию можно было объявить как
constexpr
, она должна удовлетворять нескольким требованиям. Если эти требования не выполнены, компилятор сочтет наличие спецификатора constexpr
ошибкой. Требования таковы:• все параметры должны иметь литеральный тип;
• возвращаемое значение должно иметь литеральный тип;
• тело функции может содержать только предложение
return
и ничего больше;• выражение в предложении
return
должно быть константным;• любой конструктор или оператор преобразования, встречающийся в выражении для вычисления возвращаемого значения, должен быть объявлен как
constexpr
.На самом деле, это вполне понятные требования: у компилятора должна быть возможность встроить вызов функции в константное выражение, и при этом оно должно остаться константным. Кроме того, запрещается что-либо изменять;
constexpr
-функции являются чистыми, то есть не имеют побочных эффектов.К
constexpr
-функциям, являющимся членами класса, предъявляются дополнительные требования:•
constexpr
функции-члены не могут быть виртуальными;• класс, членом которого является функция, должен иметь литеральный тип.
Для
constexpr
-конструкторов действуют другие правила:• тело конструктора должно быть пустым;
• все базовые классы должны быть инициализированы;
• все нестатические данные-члены должны быть инициализированы;
• все выражения, встречающиеся в списке инициализации членов, должны быть константными;
• конструкторы, выбранные для инициализации данных-членов и базовых классов, должны быть
constexpr
-конструкторами;• все конструкторы и операторы преобразования, используемые для конструирования данных-членов и базовых классов в соответствующем выражении инициализации, должны быть объявлены как
constexpr
.Это тот же набор правил, что и для функций, с тем отличием, что возвращаемого значения нет, а, значит, нет и предложения
return
. Вместо возврата значения конструктор инициализирует базовые классы и данные-члены в списке инициализации членов. Тривиальные копирующие конструкторы неявно объявлены как constexpr
.А.4.4. constexpr
и шаблоны
Спецификатор
constexpr
в объявлении шаблона функции или функции-члене шаблонного класса игнорируется, если типы параметров и возвращаемого значения для данной конкретизации шаблона не являются литеральными. Это позволяет писать шаблоны функций, которые становятся constexpr
-функциями, если параметры шаблона имеют подходящие типы, и обычными встраиваемыми функциями в противном случае. Например:template
constexpr T sum(T a, T b) {
return a + b;
} │
Правильно, sumconstexpr int i = sum(3, 42);←┘
constexprstd::string s =
sum(std::string("hello"), │
Правильно, но sum std::string(" world"));←┘
He constexprФункция должна удовлетворять также всем остальным требованиям, предъявляемым к
constexpr
-функциям. Нельзя включить в тело шаблона функции, объявленного как constexpr
, несколько предложений только потому, что это шаблон; компилятор сочтет это ошибкой.А.5. Лямбда-функции