Шрифт:
Интервал:
Закладка:
cout << "sorry, the input was "
"something else than 2 numbers.n";
}
}
10. На этом все. Компиляция и запуск программы дадут следующий результат:
$ ./optional
Please enter 2 integers.
> 1 2
1 + 2 + 10 = 13
11. Если мы запустим программу снова и введем не числа, то увидим сообщение об ошибке, подготовленное нами для таких случаев:
$ ./optional
Please enter 2 integers.
> 2 z
sorry, the input was something else than 2 numbers.
Как это работает
Работать с типом optional очень просто и удобно. Если мы хотим, чтобы любой тип T имел дополнительное состояние, указывающее на возможный сбой, то можем обернуть его в тип std::optional<T>.
Когда мы получаем экземпляр подобного типа откуда бы ни было, нужно проверить, он пуст или же содержит значение. Здесь поможет функция 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<Foo>, то можно написать конструкцию 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 numbersn";
}
Еще одним трюком для типа 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 <iostream>
#include <iomanip>
#include <tuple>
#include <functional>
#include <string>
#include <list>
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<size_t, string, double>;
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: