Приложение А. Краткий справочник по некоторым конструкциям языка С++

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

thread_local
(рассматриваемого в разделе А.8), все они не имеют прямого отношения к параллелизму, однако важны и (или) полезны для написания многопоточного кода. Я ограничился лишь теми конструкциями, которые либо необходимы (например, ссылки на r-значения), либо делают код проще и яснее. Поначалу разобраться в программе, где применяются эти конструкции, будет трудно, но, познакомившись с ними поближе, вы согласитесь, что, вообще говоря, включающий их код проще, а не сложнее для понимания. По мере распространения С++11 описываемые средства будут встречаться в программах все чаще.

А теперь, без дальнейших предисловий, начнем с изучения ссылок на r-значения — средства, которое широко используется в библиотеке Thread Library для передачи владения (потоками, блокировками и вообще всем на свете) от одного объекта другому.

А.1. Ссылки на r-значения

Всякий, кто программировал на С++, знаком со ссылками; в С++ ссылки служат для создания альтернативного имени существующего объекта. Любой доступ к объекту по ссылке, в том числе для модификации, приводит к манипуляциям с исходным объектом. Например:

int var = 42;  │
Создаем ссылку

int& ref = var;←┘
на var

ref = 99;      │
В результате присваивания ссылке

assert (var == 99);←┘
изменен оригинал

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

int& i = 42;

потому что 42 — это r-значение. Впрочем, это не совсем верно; всегда разрешалось связывать r-значение с константной ссылкой на l-значение:

int const& i = 42;

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

void print(std::string const& s);

print("hello");

Как бы то ни было, в стандарте C++11 официально введены ссылки на r-значения, которые связываются только с r-значениями, но не с l-значениями, и объявляются с помощью двух знаков амперсанда:

int&& i = 42;

int j = 42;

int&& k = j;

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

A.1.1. Семантика перемещения

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

Рассмотрим функцию, которая принимает в качестве параметра

std::vector
и хочет иметь его внутреннюю копию для модификации, так чтобы не затрагивать оригинал. Раньше мы для этого должны были принимать параметр как
const
-ссылку на l-значение и делать внутреннюю копию:

void process_copy(std::vector const& vec_) {

 std::vector vec(vec_);

 vec.push_back(42);

}

При этом функция может принимать как l-значения, так и r-значения, но копирование производится всегда. Однако, если добавить перегруженный вариант, который принимает ссылку на r-значение, то в этом случае можно будет избежать копирования, поскольку нам точно известно, что оригинал разрешается модифицировать:

void process_copy(std::vector&& vec) {

 vec.push_back(42);

}

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


Листинг А.1. Класс с перемещающим конструктором

class X {

private:

 int* data;


public:

 X() : data(new int[1000000]) {}


 ~X() {

  delete [] data;

 }


 X(const X& other) : ←
(1)

  data(new int[1000000]) {

  std::copy(other.data, other.data + 1000000, data);

 }


 X(X&& other) : ←
(2)

  data(other.data) {

  other.data = nullptr;

 }

};

Копирующий конструктор(1) определяется как обычно: выделяем новый блок памяти и копируем в него данные. Но теперь у нас есть еще один конструктор, который принимает ссылку на r-значение (2). Это перемещающий конструктор. В данном случае мы копируем только указатель на данные, а в объекте

other
остается нулевой указатель. Таким образом, мы обошлись без выделения огромного блока памяти и сэкономили время на копировании данных из r-значения.

В классе

X
перемещающий конструктор — всего лишь оптимизация, но в ряде случаев такой конструктор имеет смысл определять, даже когда копирующий конструктор не предоставляется. Например, идея
std::unique_ptr<>
в том и заключается, что любой ненулевой экземпляр является единственным указателем на свой объект, поэтому копирующий конструктор лишен смысла. Однако же перемещающий конструктор позволяет передавать владение указателем от одного объекта другому, поэтому
std::unique_ptr<>
можно использовать в качестве возвращаемого функцией значения — указатель перемещается, а не копируется.

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

static_cast
, либо путем вызова функции
std::move()
:

X x1;

X x2 = std::move(x1);

X x3 = static_cast(x2);

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

void do_stuff(X&& x_) {

 X a(x_); ←
Копируется

 X b(std::move(x_)); ←
Перемещается

}        │
r-значение связывается

do_stuff(X());←┘
со ссылкой на r -значение

X x;     │
Ошибка, l -значение нельзя связывать

do_stuff(x);←┘
со ссылкой на r -значение

Семантика перемещения сплошь и рядом используется в библиотеке Thread Library — и в случаях, когда копирование не имеет смысла, но сами ресурсы можно передавать, и как оптимизация, чтобы избежать дорогостоящего копирования, когда исходный объект все равно будет уничтожен. Один пример мы видели в разделе 2.2, где

std::move()
использовалась для передачи экземпляра
std::unique_ptr<>
новому потоку, а второй — в разделе 2.3, когда рассматривали передачу владения потоком от одного объекта
std::thread
другому.

Ни один из классов

std::thread
,
std::unique_lock<>
,
std::future<>
,
std::promise<>
,
std::packaged_task<>
не допускает копирования, но в каждом из них имеется перемещающий конструктор, который позволяет передавать ассоциированный ресурс другому экземпляру и возвращать объекты этих классов из функций. Объекты классов
std::string
и
std::vector<>
можно копировать, как и раньше, но дополнительно они обзавелись перемещающими конструкторами и перемещающими операторами присваивания, чтобы избежать копирования данных из r-значений.

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

std::thread
, содержимое которого перемещено, эквивалентен объекту
std::thread
, сконструированному по умолчанию, а экземпляр
std::string
, бывший источником перемещения, все же находится в согласованном состоянии, хотя не дается никаких гарантий относительно того, что это за состояние (в терминах длины строки или содержащихся в ней символов).

А.1.2. Ссылки на r-значения и шаблоны функций

Еще один нюанс имеет отношение к использованию ссылок на r-значения в качестве параметров шаблона функции: если параметр функции — ссылка на r-значение типа параметра шаблона, механизм автоматического выведения типа аргумента шаблона заключает, что тип — это ссылка на l-значение, если функции передано l-значение, или обычный не-ссылочный тип, если передано r-значение. Фраза получилась довольно запутанной, поэтому приведём пример. Рассмотрим такую функцию:

template

void foo(T&& t) {}

Если при вызове передать ей r-значение, как показано ниже, то в качестве

T
выводится тип этого значения:

foo(42);

foo(3.14159);

fоо(std::string());

Но если вызвать

foo
, передав l-значение, то механизм выведения типа решит, что
T
— ссылка на l-значение:

int i = 42;

foo(i);

Поскольку объявлено, что параметр функции имеет тип

T&&
, то получается, что это ссылка на ссылку, и такая конструкция трактуется как обычная одинарная ссылка. Таким образом, сигнатура функции
foo()
такова:

void foo(int& t);

Это позволяет одному шаблону функции принимать параметры, являющиеся как l-, так и r-значениями. В частности, это используется в конструкторе

std::thread
(см. разделы 2.1 и 2.2), чтобы в случае, когда переданный допускающий вызов объект является r-значением, его можно было бы не копировать, а переместить во внутреннюю память.

А.2. Удаленные функции

Иногда операция копирования класса лишена смысла. Типичный пример —

std::mutex
. Действительно, что должно было бы получиться в результате копирования мьютекса? Другой пример —
std::unique_lock<>
, экземпляр этого класса является единственным владельцем удерживаемой им блокировки. Честное копирование в этом случае означало бы, что у блокировки два владельца, а это противоречит определению. Передача владения, описанная в разделе А.1.2, имеет смысл, но это не копирование. Уверен, вы назовете и другие примеры.

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

class no_copies {

public:

 no_copies(){}


private:

 no_copies(no_copies const&); ←
Реализаций нет

 no_copies& operator=(no_copies const&);

};


no_copies a; ←
He компилируется

no_copies b(a);

Комитет, разрабатывавший стандарт C++11, конечно, знал об этой идиоме, но счел ее не совсем честным приёмом. Поэтому было решено предоставить более общий механизм, применимый и к другим случаям: объявить функцию удаленной, включив в ее объявление конструкцию

= delete
. Тогда класс
no_copies
можно записать в виде:

class no_copies {

public:

 no_copies() {}

 no_copies(no_copies const&) = delete;

 no_copies& operator=(no_copies const&) = delete;

};

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

Если, удалив копирующие конструктор и оператор присваивания, вы явно напишете перемещающие конструктор и оператор присваивания, то класс будет допускать только перемещение — как, например,

std::thread
и
std::unique_lock<>
. В следующем листинге приведен пример такого класса.


Листинг А.2. Простой тип, допускающий только перемещение

class move_only {

 std::unique_ptr data;


public:

 move_only(const move_only&) = delete;

 move_only(move_only&& other):

  data(std::move(other.data)) {}

 move_only& operator=(const move_only&) = delete;

 move_only& operator=(move_only&& other) {

  data = std::move(other.data);

  return *this;

 }

};


move_only m1;   │
Ошибка, копирующий конструктор объявлен

move_only m2(m1);←┘
удаленным

move_only m3(std::move(m1));←┐
правильно, имеется переме-

щающий конструктор

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

std::move()
или оператором
static_cast
.

Спецификатор

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

void foo(short);

void foo(int) = delete;

Любую попытку вызвать

foo
с параметром типа
int
компилятор встретит в штыки, так что вызывающей программе придётся явно привести параметр к типу
short
:

foo(42); ←
Ошибка, перегрузка для int удалена

foo((short)42); ←
Правильно

А.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), чтобы разрешить статическую инициализацию.

А.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() не объявлена как constexpr

A.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;

}               │
Правильно, sum

constexpr int i = sum(3, 42);←┘
constexpr

std::string s =

 sum(std::string("hello"),  │
Правильно, но sum

   std::string(" world"));←┘
He constexpr

Функция должна удовлетворять также всем остальным требованиям, предъявляемым к

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

А.5. Лямбда-функции

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

std::condition_variable
(как в примере из раздела 4.1.1), потому что дает возможность кратко выразить семантику в терминах доступных в данной точке переменных, а не запоминать необходимое состояние в переменных-членах класса с оператором вызова.

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

[] { ←
Лямбда-выражение начинается с []

 do_stuff();   │
Конец определения

 do_more_stuff();│
лямбда-выражения

} ();      ←┘
и его вызов

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

std::cout
, разделяя их символами новой строки:

std::vector data = make_data();

std::for_each(data.begin(), data.end(),

 [](int i){std::cout << i << "\n";});

С возвращаемыми значениями всё почти так же просто. Если тело лямбда-функции состоит из единственного предложения

return
, то тип возвращаемого ей значения совпадает с типом возвращаемого выражения. Например, такую простую лямбда-функцию можно было бы использовать для проверки флага, ожидаемого условной переменной
std::condition_variable
(см. раздел 4.1.1).


Листинг А.4. Простая лямбда-функция с выводимым типом возвращаемого значения

std::condition_variable cond;

 bool data_ready;

 std::mutex m;


void wait_for_data() {

 std::unique_lock lk(m);

 cond.wait(lk, []{return data_ready;}); ←
(1)

}

Тип значения, возвращаемого лямбда-функцией, которая передана

cond.wait()
(1), выводится из типа переменной
data_ready
, то есть совпадает с
bool
. Когда условная переменная получает сигнал, она вызывает эту лямбда-функцию, захватив предварительно мьютекс, и
wait()
возвращает управление, только если
data_ready
равно
true
.

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

return
? В таком случае тип возвращаемого значения следует задать явно. Это можно сделать и тогда, когда тело функции состоит из единственного предложения
return
, но обязательно, если тело более сложное. Для задания типа возвращаемого значения нужно поставить после списка параметров функции стрелку (
->
), а за ней указать тип. Если лямбда-функция не имеет параметров, то список параметров (пустой) все равно необходим, иначе задать тип возвращаемого значения невозможно. Таким образом, предикат, проверяемый условной переменной, можно записать так:

cond.wait(lk, []()->bool{ return data_ready; });

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

cond.wait(lk, []()->bool {

 if (data_ready) {

  std::cout << "Данные готовы" << std::endl;

  return true;

 } else {

  std::cout <<

  "Данные не готовы, продолжаю ждать" << std::endl;

  return false;

 }

});

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

A.5.1. Лямбда-функции, ссылающиеся на локальные переменные

Лямбда-функции с лямбда-интродуктором вида

[]
не могут ссылаться на локальные переменные из объемлющей области видимости; им разрешено использовать только глобальные переменные и то, что передано в параметрах. Чтобы получить доступ к локальной переменной, ее нужно захватить (capture). Проще всего захватить все переменные в локальной области видимости, указав лямбда-интродуктор вида
[=]
. Теперь лямбда-функция может получить доступ к копиям локальных переменных на тот момент, когда эта функция была создана.

Рассмотрим этот механизм на примере следующей простой функции:

std::function make_offseter(int offset) {

 return [=](int j){return offset+j;};

}

При каждом вызове

make_offseter
с помощью обертки
std::function<>
создается новый содержащий лямбда-функцию объект. Возвращенная функция добавляет указанное смещение к любому переданному ей параметру. Например, следующая программа

int main() {

 std::function offset_42 = make_offseter(42);

 std::function offset_123 = make_offseter(123);

 std::cout <<

  offset_42(12) << "," << offset_123(12) << std::endl;

 std::cout <<

  offset_42(12) << "," << offset_123(12) << std::endl;

}

два раза выведет числа

54, 135
, потому что функция, возвращенная после первого обращения к
make_offseter
, всегда добавляет 42 к переданному ей аргументу Напротив, функция, возвращенная после второго обращения к
make_offseter
, добавляет к своему аргументу 123. Это самый безопасный вид захвата локальных переменных — все значения копируются, поэтому лямбда-функцию можно вернуть и вызывать вне контекста функции, в которой она была создана. Но это не единственно возможное решение, можно захватывать локальные переменные и по ссылке. В таком случае попытка вызвать лямбда-функцию после того, как переменные, на которые указывают ссылки, были уничтожены в результате выхода из области видимости объемлющей их функции или блока, приведёт к неопределённому поведению, точно так же, как обращение к уничтоженной переменной в любом другом случае.

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

[&]
:

int main() {

 int offset = 42;         ←
(1)

 std::function offset_a =

  [&](int j){return offset + j;};←
(2)

 offset = 123;          ←
(3)

 std::function offset_b =

  [&](int j){return offset + j;};←
(4)

 std::cout <<

 offset_a(12) << "," << offset_b(12) << std::endl; ←
(5)

 offset = 99;                    ←
(6)

 std::cout <<

  offset_a(12) << "," << offset_b(12) << std::endl; ←
(7)

}

Если функция

make_offseter
из предыдущего примера захватывала копию смещения
offset
, то функция
offset_a
в этом примере, начинающаяся интродуктором
[&]
, захватывает
offset
по ссылке (2). Неважно, что начальное значение
offset
было равно 42 (1); результат вызова
offset_a(12)
зависит от текущего значения
offset
. Значение
offset
было изменено на 123 (3) перед порождением второй (идентичной) лямбда-функции
offset_b
(4), но эта вторая функция снова производит захват по ссылке, поэтому результат, как и прежде, зависит от текущего значения
offset
.

Теперь при печати первой строки (5),

offset
всё еще равно 123, поэтому печатаются числа
133, 135
. Однако к моменту печати второй строки (7)
offset
стало равно 99 (6), поэтому печатается
111, 111
. И
offset_a
, и
offset_b
прибавляют текущее значение
offset
(99) к переданному аргументу (12).

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

[=]
, но после знака равенства перечислите переменные, захватываемые по ссылке, предпослав им знаки амперсанда. В следующем примере печатается
1239
, потому что переменная
i
копируется в лямбда-функцию, a
j
и
k
захватываются по ссылке:

int main() {

 int i=1234, j=5678, k=9;

 std::function f=[=,&j,&k] {return i+j+k;};

 i = 1;

 j = 2;

 k = 3;

 std::cout << f() << std::endl;

}

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

[&]
, а после знака амперсанда перечислите переменные, захватываемые по значению. В следующем примере печатается
5688
, потому что
i
захватывается по ссылке, a
j
и
k
копируются:

int main() {

 int i=1234, j=5678, k= 9;

 std::function f=[&,j,k] {return i+j+k;};

 i = 1;

 j = 2;

 k = 3;

 std::cout << f() << std::endl;

}

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

=
или
&
и просто перечислить захватываемые переменные, предпослав знак амперсанда тем, что должны захватываться по ссылке, а не по значению. В следующем примере печатается
5682
, потому что
i
и
k
захвачены по ссылке, a
j
скопирована

int main() {

 int i=1234, j=5678, k=9;

 std::function f=[&i, j, &k] {return i+j+k;};

 i =
1;

 j = 2;

 k = 3;

 std::cout << f() << std::endl;

}

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

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

struct X {

 int some_data;


 void foo(std::vector& vec) {

  std::for_each(vec.begin(), vec.end(),

  [this](int& i){ i += some_data; });

 }

};

В контексте параллелизма лямбда-функции особенно полезны для задания предикатов функции

std::condition_variable::wait()
(см. раздел 4.1.1) и в сочетании с
std::packaged_task<>
(раздел 4.2.1) или пулами потоков для упаковки небольших задач. Их можно также передавать конструктору
std::thread
в качестве функций потока (раздел 2.1.1) и в качестве исполняемой функции в таких параллельных алгоритмах, как
parallel_for_each()
(раздел 8.5.1).

А.6. Шаблоны с переменным числом параметров

Функции с переменным числом параметров, например

printf
, используются уже давно, а теперь появились и шаблоны с переменным числом параметров (variadic templates). Такие шаблоны применяются во многих местах библиотеки С++ Thread Library. Например, конструктор
std::thread
для запуска потока (раздел 2.1.1) — это шаблон функции с переменным числом параметров, a
std::packaged_task<>
(раздел 4.2.2) — шаблон класса с переменным числом параметров. С точки зрения пользователя, достаточно знать, что шаблон принимает неограниченное количество параметров, но если вы хотите написать такой шаблон или просто любопытствуете, как это работает, то детали будут небезынтересны.

При объявлении шаблонов с переменным числом параметров, по аналогии с обычными функциями, употребляется многоточие (

...
) в списке параметров шаблона:

template

class my_template {};

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

std::packaged_task<>
(раздел 4.2.1) — это простой шаблон с единственным параметром:

template

class packaged_task;

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

template

class packaged_task;

Именно внутри частичной специализации и содержится реальное определение класса; в главе 4 мы видели, что для объявления задачи, которая принимает параметры типа

std::string
и
double
и возвращает результат в виде объекта
std::future
, можно написать
std::packaged_task
.

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

ReturnType
), так и переменные (
Args
). Второе свойство — это использование
Args...
в списке аргументов специализации шаблона для обозначения того, что здесь должны быть перечислены фактические типы, подставляемые вместо
Args
в точке конкретизации шаблона. На самом деле, поскольку это частичная специализация, то работает она, как сопоставление с образцом; типы, встречающиеся в контексте конкретизации, запоминаются как
Args
. Переменное множество параметров
Args
называется пакетом параметров (parameter pack), а конструкция
Args...
— расширением пакета.

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

std::packaged_task
параметром
ReturnType
является
my_class
, а пакет параметров
Args
пуст. С другой стороны, в конкретизации
std::packaged_task
параметр
ReturnType
— это
void
, и
Args
— список, состоящий из элементов
int
,
double
,
my_class&
,
std::string*
.

A.6.1. Расширение пакета параметров

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

template

struct dummy {

 std::tuple data;

};

В данном случае единственная переменная-член

data
представляет собой конкретизацию
std::tuple<>
, содержащую все заданные типы, то есть в классе
dummy
имеется член типа
std::tuple
. Расширение пакета можно комбинировать с обычными типами:

template

struct dummy2 {

 std::tuple data;

};

На этот раз класс

tuple
имеет дополнительный (первый) член типа
std::string
. Есть еще одна красивая возможность: разрешается определить образец, в который будут подставляться все элементы расширения пакета. Для этого в конце образца размещается многоточие
...
, обозначающее расширение пакета. Например, вместо кортежа элементов тех типов, которые перечислены в пакете параметров, можно создать кортеж указателей на такие типы или даже кортеж интеллектуальных указателей
std::unique_ptr<>
на них:

template

struct dummy3 {

 std::tuple pointers;

 std::tuple ...> unique_pointers;

};

Типовое выражение может быть сколь угодно сложным при условии, что в нем встречается пакет параметров и после него находится многоточие

...
, обозначающее расширение. Во время расширения пакета параметров каждый элемент пакета подставляется в типовое выражение и порождает соответственный элемент в результирующем списке. Таким образом, если пакет параметров
Params
содержит типы
int
,
int
,
char
, то расширение выражения
std::tuple, double> ... >
дает
std::tuple, double>
,
std::pair, double>
,
std::pair, double>>
. Если расширение пакета используется в качестве списка аргументов шаблона, то шаблон не обязан иметь переменные параметры, но если таковых действительно нет, то размер пакета должен быть в точности равен количеству требуемых параметров шаблона:

template

struct dummy4 {

 std::pair data;

};          │
Правильно, данные имеют

dummy4 a;←┘
вид std::pair

dummy4 b; ←
Ошибка, нет второго типа

dummy4 с;←
Ошибка, слишком много типов

Еще один способ применения расширения пакета — объявление списка параметров функции:

template

void foo(Args ... args);

При этом создается новый пакет параметров

args
, являющийся списком параметров функции, а не списком типов, и его можно расширить с помощью
...
, как и раньше. Теперь для объявления параметров функции можно использовать образец, в который производится подстановка типов из расширения пакета, — точно так же, как при подстановке расширения пакета в образец в других местах. Например, вот как это применяется в конструкторе
std::thread
, чтобы все аргументы функции принимались по ссылке на r-значение (см. раздел А.1):

template

thread::thread(CallableType&& func, Args&& ... args);

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

std::forward<>
для сохранения свойства «является r-значением» переданных функции аргументов:

template

void bar(ArgTypes&& ... args) {

 foo(std::forward(args)...);

}

Отметим, что в этом случае расширение пакета содержит как пакет типов

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

int i;

bar(i, 3.141, std::string("hello "));

то расширение примет такой вид:

template<>

void bar(

 int& args_1,

 double&& args_2,

 std::string&& args_3) {

 foo(std::forward(args_1),

 std::forward(args_2),

 std::forward(args_3));

}

и, следовательно, первый аргумент правильно передается функции

foo
как ссылка на l-значение, а остальные — как ссылки на r-значения.

И последнее, что можно сделать с пакетом параметров, — это узнать его размер с помощью оператора

sizeof...
. Это совсем просто:
sizeof...(p)
возвращает число элементов в пакете параметров
p
. Неважно, является ли
p
пакетом параметров-типов или пакетом аргументов функции, — результат будет одинаковый. Это, пожалуй, единственный случай, где пакет параметров употребляется без многоточия, поскольку многоточие уже является частью оператора
sizeof...
. Следующая функция возвращает число переданных ей аргументов:

template

unsigned count_args(Args ... args) {

 return sizeof... (Args);

}

Как и для обычного оператора

sizeof
, результатом
sizeof...
является константное выражение, которое, следовательно, можно использовать для задания границ массива и т.п.

А.7. Автоматическое выведение типа переменной

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

std::map> m;

std::map>::iterator

iter = m.find("my key");

Традиционно для решения этой проблемы использовались псевдонимы типов (

typedef
), позволяющие сократить длину идентификатора типа и избавиться от потенциальных проблем несовместимости типов. Этот способ работает и в C++11, но появился и новый: если переменная инициализируется в объявлении, то в качестве ее типа можно указать
auto
. Тогда компилятор автоматически выведет тип переменной из типа инициализатора. Следовательно, приведенный выше пример итератора можно записать и так:

auto iter = m.find("my key");

Спецификатор

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

auto i = 42;     // int

auto& j = i;     // int&

auto const k = i;  // int const

auto* const p = &i; // int * const

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

Какое-то-типовое-выражение-включающее-auto

var = some-expression;

переменная

var
имеет тот же тип, который был бы выведен, если бы она встречалась в качестве параметра шаблона функции, объявленного с таким же типовым выражением, только
auto
заменяется именем типового параметра шаблона:

template

void f(type-expression var);

f(some-expression);

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

int some_array[45];

auto p = some_array; // int*

int& r = *p;

auto x = r;      // int

auto& y = r;     // int&

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

А.8. Поточно-локальные переменные

У поточно-локальной переменной имеется отдельный экземпляр в каждом потоке программы. Для объявления поточно-локальной переменной служит ключевое слово

thread_local
. Поточно-локальными могут быть переменные с областью видимости пространства имен, статические члены классов и локальные переменные. Говорят, что они имеют потоковое время жизни (thread storage duration):

thread_local int x;←┐
Поточно-локальная переменная в

области видимости пространства

имен

class X               │
Поточно-локальная

{                  │
статическая пере-

 static thread_local std::string s;←┘
менная-член класса

};

Необходимо

static thread_local std::string X::s;
определение X::s


void foo() {

Поточно-локальная

 thread_local std::vector v;←┘
локальная переменная

}

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

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

У поточно-локальных переменных есть и другие общие черты со статическими переменными — они инициализируются нулями перед последующей инициализацией (например, динамической) и, если конструктор поточно-локальной переменной возбуждает исключение, то вызывается функция

std::terminate()
, которая аварийно завершает приложение.

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

std::terminate()
, как и при конструировании.

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

std::exit()
или возвращается из
main()
(что эквивалентно вызову
std::exit()
со значением, которое вернула
main()
). Если другие потоки продолжают работать, когда приложение завершается, то деструкторы принадлежащих им поточно-локальных переменных не вызываются.

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

А.9. Резюме

В этом приложении мы смогли лишь пробежаться по верхам новых языковых средств, появившихся в стандарте C++11, поскольку нас интересовали лишь те возможности, которые активно используются в библиотеке Thread Library. Из других средств стоит отметить статические утверждения, строго типизированные перечисления, делегирующие конструкторы, поддержку Unicode, псевдонимы шаблонов, новую универсальную последовательность инициализации, а также ряд других, более мелких изменений. Подробное описание всех новых средств выходит далеко за рамки этой книги и, пожалуй, заслуживает отдельного тома. Лучший из существующих на данный момент обзор всего множества изменений, наверное, приведён в FAQ'e по C++11[21] Бьярна Страуструпа, хотя популярные учебники по С++ в скором времени, вероятно, будут переизданы с учетом всех новшеств.

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

Загрузка...