optional
очень просто и удобно. Если мы хотим, чтобы любой тип T
имел дополнительное состояние, указывающее на возможный сбой, то можем обернуть его в тип std::optional
.Когда мы получаем экземпляр подобного типа откуда бы ни было, нужно проверить, он пуст или же содержит значение. Здесь поможет функция
optional::has_value()
. Если она возвращает значение true
, то можно получить доступ к этому значению. Это позволяет сделать вызов T& optional::value()
.Вместо того чтобы всегда использовать конструкции
if (x.has_value()) {...}
и x.value()
, можно применить конструкции if (x) { }
и *x
. В типе std::optional
определено неявное преобразование к типу bool
и operator*
так, что работа с типом optional
похожа на работу с указателем.Существует еще один удобный вспомогательный оператор — это
->
. Если у нас есть тип struct Foo { int a; string b; }
и нужно получить доступ к одному из его членов с помощью переменной x
типа optional
, то можно написать конструкцию x->a
или x->b
. Конечно, сначала следует проверить, содержит ли х
значение. Если мы попробуем получить доступ к объекту типа optional
, который не содержит значения, то будет сгенерирована ошибка std::logic_error
. Таким образом, нельзя работать с большим количеством необязательных экземпляров, не проверяя их.С помощью блока
try-catch
можно писать код в следующей форме:
cout << "Please enter 3 numbers:\n";
try {
cout << "Sum: "
<< (*read_int() + *read_int() + *read_int())
<< '\n';
} catch (const std::bad_optional_access &) {
cout << "Unfortunately you did not enter 3 numbers\n";
}
Еще одним трюком для типа
std::optional
является optional::value_or
. Это поможет, когда мы хотим взять необязательное значение и, если оно окажется пустым, откатить его к значению, заданному по умолчанию. Можно решить эту задачу с помощью одной емкой строки x = optional_var.value_or(123)
, где 123
— значение по умолчанию.Применяем функции для кортежей
Начиная с C++11, STL предоставляет тип
std::tuple
. Он позволяет время от времени объединять несколько значений в одну переменную и получать к ним доступ. Кортежи есть во многих языках программирования, и в некоторых примерах данной книги мы уже работали с этим типом, поскольку он крайне гибок.Однако иногда мы помещаем в кортеж значения, а затем хотим вызвать функции, передав в них его отдельные члены. Распаковывать члены по отдельности для каждого аргумента функции очень утомительно (а кроме того, могут возникнуть ошибки, если где-то вкрадется опечатка). Это выглядит так:
func(get<0>(tup)
, get<1>(tup)
, get<2>(tup), ...);
.Ниже мы рассмотрим, как упаковывать значения в кортежи и ловко распаковывать из них, чтобы вызвать функции, которые не знают о кортежах.
Как это делается
В этом примере мы реализуем программу, которая упаковывает значения в кортежи и распаковывает из них. Затем увидим, как вызывать функции, ничего не знающие о кортежах, и передавать в них значения из кортежей.
1. Сначала включим множество заголовочных файлов и объявим об использовании пространства имен
std
:
#include
#include
#include
#include
#include
#include
using namespace std;
2. Определим функцию, которая принимает несколько параметров, описывающих студента, и выводит их на экран. Многие устаревшие интерфейсы и интерфейсы функций языка С выглядят похоже:
static void print_student(size_t id, const string &name, double gpa)
{
cout << "Student " << quoted(name)
<< ", ID: " << id
<< ", GPA: " << gpa << '\n';
}
3. В самой программе определим тип кортежа динамически и заполним его осмысленными данными о студентах:
int main()
{
using student = tuple;
student john {123, "John Doe"s, 3.7};
4. Чтобы вывести такой объект на экран, можем разбить его на отдельные члены и вызвать функцию
print_student
для этих отдельных переменных:
{
const auto &[id, name, gpa] = john;
print_student(id, name, gpa);
}
cout << "-----\n";
5. Создадим несколько студентов в виде списка инициализаторов для кортежей:
auto arguments_for_later = {
make_tuple(234, "John Doe"s, 3.7),
make_tuple(345, "Billy Foo"s, 4.0),
make_tuple(456, "Cathy Bar"s, 3.5),
};
6. Мы все еще можем относительно комфортно вывести их на экран, но, чтобы разбить кортеж на части, следует знать, сколько элементов в нем содержится. Если нужно писать подобный код, то понадобится также реструктурировать его в случае изменения интерфейса вызова функции:
for (const auto &[id, name, gpa] : arguments_for_later) {
print_student(id, name, gpa);
}
cout << "-----\n";
7. Можно сделать лучше. Даже не зная типов аргументов функции
print_student
или количества членов кортежа, описывающего студентов, можно направить содержимое кортежа непосредственно в функцию с помощью std::apply
. Она принимает указатель на функцию или объект функции и кортеж, а затем распаковывает кортеж, чтобы вызвать функцию, передав в нее в качестве параметров члены кортежа:
apply(print_student, john);
cout << " \n";
8. Конечно, все это прекрасно работает и в цикле:
for (const auto &args : arguments_for_later) {
apply(print_student, args);
}
cout << "-----\n";
}
9. Компиляция и запуск программы покажут, что работают оба подхода, как мы и предполагали:
$ ./apply_functions_on_tuples
Student "John Doe", ID: 123, GPA: 3.7
-----
Student "John Doe", ID: 234, GPA: 3.7
Student "Billy Foo", ID: 345, GPA: 4
Student "Cathy Bar", ID: 456, GPA: 3.5
-----
Student "John Doe", ID: 123, GPA: 3.7
-----
Student "John Doe", ID: 234, GPA: 3.7
Student "Billy Foo", ID: 345, GPA: 4
Student "Cathy Bar", ID: 456, GPA: 3.5
-----
Как это работает
Функция
std::apply
— это вспомогательная функция времени компиляции, которая позволяет нам работать, не имея сведений обо всех типах данных, которые появляются в нашем коде.Допустим, у нас есть кортеж
t
со значениями (123, "abc"s, 456.0)
. Он имеет тип tuple
. Вдобавок предположим, что у нас есть функция f
с сигнатурой int f(int, string, double)
(типы также могут быть ссылками).Затем можно написать конструкцию