Параллельное программирование на С++ в действии — страница 38 из 53

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

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. Автоматическое выведение типа переменной