& standard_string() {
static std::string s {"some standard string"};
return s;
}
};
Подобным образом вы можете совершенно легально включить заголовочный файл в несколько модулей и при этом получать доступ к одному и тому же экземпляру отовсюду. Однако объект не создается немедленно при старте программы — это происходит только при первом вызове функции-геттера. В некоторых случаях это может оказаться проблемой. Представьте, будто нужно, чтобы конструктор статического объекта, доступного глобально, при запуске программы выполнял некую важную операцию (в точности как наш класс-пример), но мы не получаем желаемого из-за вызова геттера ближе к концу программы.
Проблему можно решить еще одним способом: сделав класс
foo
шаблонным и воспользовавшись преимуществами шаблонов.В C++17 оба варианта становятся неактуальны.
Реализуем вспомогательные функции с помощью выражений свертки
Начиная с C++11, в языке появились пакеты параметров для шаблонов с переменным количеством аргументов. Такие пакеты позволяют реализовывать функции, принимающие переменное количество параметров. Иногда эти параметры объединяются в одно выражение, чтобы на его основе можно было получить результат работы функции. Решение этой задачи значительно упростилось с выходом C++17, где появились выражения свертки.
Как это делается
Реализуем функцию, которая принимает переменное количество параметров и возвращает их сумму.
1. Сначала определим ее сигнатуру:
template
auto sum(Ts ts);
2. Теперь у нас есть пакет параметров
ts
, функция должна распаковать все параметры и просуммировать их с помощью выражения свертки. Допустим, мы хотим воспользоваться каким-нибудь оператором (в нашем случае +
) вместе с ..., чтобы применить его ко всем значениям пакета параметров. Для этого нужно взять выражение в скобки:
template
auto sum(Ts ts)
{
return (ts + ...);
}
3. Теперь можно вызвать функцию следующим образом:
int the_sum {sum(1, 2, 3, 4, 5)}; // Значение: 15
4. Она работает не только с целочисленными типами; можно вызвать ее для любого типа, реализующего оператор
+
, например std::string
:
std::string a {"Hello "};
std::string b {"World"};
std::cout << sum(a, b) << '\n'; // Вывод: Hello World
Как это работает
Только что мы написали код, в котором с помощью простой рекурсии бинарный оператор (
+
) применяется к заданным параметрам. Как правило, это называется сверткой. В C++17 появились выражения свертки, которые помогают выразить ту же идею и при этом писать меньше кода.Подобное выражение называется унарной сверткой. C++17 позволяет применять к пакетам параметров свертки следующие бинарные операторы:
+
, –
, *
, /
, %
, ^
, &
, |
,=
, <
, >
, <<
, >>
, +=
, –=
, *=
, /=
, %=
, ^=
, &=
, |=
, <<=
, >>=
, ==
, !=
, <=
, >=
, &&
, ||
, ,
, .*
, –>*
.Кстати, в нашем примере кода неважно, какую использовать конструкцию, (
ts +
…) или (… + ts
);. Они обе работают так, как нужно. Однако между ними есть разница, которая может иметь значение в других случаях: если многоточие …
находится с правой стороны оператора, то такое выражение называется правой сверткой. Если же оно находится с левой стороны, то это левая свертка.В нашем примере с суммой левая унарная свертка разворачивается в конструкцию
1+(2+(3+(4+5)))
, а правая унарная свертка развернется в (((1+2)+3)+4)+5
. В зависимости от того, какой оператор используется, могут проявиться нюансы. При добавлении новых чисел ничего не меняется.
Дополнительная информация
Если кто-то вызовет функцию
sum()
и не передаст в нее аргументы, то пакет параметров произвольной длины не будет содержать значений, которые могут быть свернуты. Для большинства операторов такая ситуация считается ошибкой (но для некоторых — нет, вы увидите это чуть позже). Далее нужно решить, генерировать ошибку или же вернуть конкретное значение. Очевидным решением будет вернуть значение 0
.Это делается так:
template
auto sum(Ts ... ts)
{
return (ts + ... + 0);
}
Таким образом, вызов
sum()
возвращает значение 0
, а вызов sum(1, 2, 3)
— значение (1+(2+(3+0)))
. Подобные свертки с начальным значением называются бинарными.Кроме того, обе конструкции,
(ts + ... + 0)
и (0 + ... + ts)
, работают как полагается, но такая бинарная свертка становится правой или левой соответственно. Взгляните на рис. 1.2.При использовании бинарных сверток для решения такой задачи, когда аргументы отсутствуют, очень важны нейтральные элементы — в нашем случае сложение любого числа с нулем ничего не меняет, что делает
0
нейтральным элементом. Поэтому можно добавить 0
к любому выражению свертки с помощью операторов +
или –
. Если пакет параметров пуст, это приведет к возврату функцией значения 0
. С математической точки зрения это правильно. С точки зрения реализации нужно определить, что именно является правильным в зависимости от наших требований.Тот же принцип применяется и к умножению. Здесь нейтральным элементом станет
1
:
template
auto product(Ts ts)
{
return (ts * ... * 1);
}
Результат вызова
product(2, 3)
равен 6
, а результат вызова product()
без параметров равен 1
.В логических операторах
И (&&)
и ИЛИ (||)
появились встроенные нейтральные элементы. Свертка пустого пакета параметров с оператором &&
заменяется на true
, а свертка пустого пакета с оператором ||
— на false
.Еще один оператор, для которого определено значение по умолчанию, когда он используется для пустых пакетов параметров, — это оператор «запятая» (
,
), заменяемый на void()
.Давайте взглянем на другие вспомогательные функции, которые можно реализовать с помощью этих механизмов.
Соотнесение диапазонов и отдельных элементов
Как насчет функции, которая определяет, содержит ли диапазон хотя бы одно из значений, передаваемых в пакете параметров с переменной длиной:
template
auto matches(const R& range, Ts ... ts)
{
return (std::count(std::begin(range), std::end(range), ts) + ...);
}
Вспомогательная функция использует функцию
std::count
из библиотеки STL. Она принимает три параметра: первые два представляют собой начальный и конечный итераторы того или иного итерабельного промежутка, а третий параметр — это значение, с которым будут сравниваться все элементы промежутка. Метод std::count
возвращает количество всех элементов внутри диапазона, равных третьему параметру.В нашем выражении свертки мы всегда передаем в функцию
std::count
начальный