А.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, чтобы инициализация мьютекса всегда производилась на этапе статической инициализации.