C++17 STL Стандартная библиотека шаблонов — страница 81 из 119

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)
(типы также могут быть ссылками).

Затем можно написать конструкцию