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

ops
, мы получаем лямбда-объект, принимающий в качестве параметров два операнда —
l
и
r
:


    try {

      const auto & op (ops.at(*it));

      const double result {op(l, r)};

      val_stack.push(result);

    }


11. Мы окружили математическую часть приложения блоком

try
, поэтому можем отловить потенциально возникающие исключения. Вызов функции
at
для контейнера
map
сгенерирует исключение
out_of_range
, если пользователь даст команду выполнить математическую операцию, о которой мы не знаем. В таком случае мы повторно сгенерируем другое исключение, сообщающее о том, что полученный аргумент некорректен (
invalid argument
), и содержащее строку, которая оказалась неизвестной для нас:


    catch (const out_of_range &) {

      throw invalid_argument(*it);

    }


12. На этом все. Когда цикл прекратит свою работу, у нас в стеке будет итоговый результат. Так что мы просто вернем его. (В тот момент можно проверить, равен ли размер стека единице. Если нет, значит, некоторые операции были пропущены.)


    }

  }

  return val_stack.top();

}


13. Теперь можно воспользоваться нашим анализатором ОПН. Для этого обернем стандартный поток ввода данных с помощью пары итераторов

std::istream_iterator
и передадим его функции-анализатору ОПН. Наконец, выведем результат на экран:


int main()

{

  try {

    cout << evaluate_rpn(istream_iterator{cin}, {})

<< '\n';

  }


14. Опять же эту строку мы обернули в блок

try
, поскольку существует вероятность, что пользовательские данные содержат операции, которые у нас не реализованы. В таких ситуациях нужно перехватывать генерируемое исключение и выводить на экран сообщение об ошибке:


  catch (const invalid_argument &e) {

    cout << "Invalid operator: " << e.what() << '\n';

  }

}


15. После компиляции программы можно с ней поэкспериментировать. Входные данные

"3 1 2 + * 2 /"
представляют собой выражение
(3 * (1 + 2) ) / 2
, которое приложение преобразует к корректному результату:


$ echo "3 1 2 + * 2 /" | ./rpn_calculator

4.5


Как это работает

Весь пример строится на помещении операндов в стек до тех пор, пока мы не найдем во входных данных операцию. В этой ситуации мы выталкиваем два последних операнда из стека, применяем к ним операцию и помещаем результат обратно в стек. Чтобы понять весь код примера, важно разобраться, как мы различаем операнды и операторы, работаем со стеком, а также выбираем и применяем правильную математическую операцию.


Работа со стеком

Мы помещаем элементы в стек с помощью функции

push
класса
std::stack
:


val_stack.push(val);


Выталкивание элемента из стека выглядит чуть сложнее, поскольку нам пришлось реализовывать для этого лямбда-выражение, которое принимает ссылку на объект

val_stack
. Взглянем на код, только теперь добавим к нему комментарии:


  auto pop_stack ([&](){

    auto r (val_stack.top()); // Получаем копию верхнего значения

    val_stack.pop();          // Удаляем верхнее значение

    return r;                 // Возвращаем копию

  }

);


Это лямбда-выражение необходимо для получения верхнего значения стека удаления из самого адаптера всего за один шаг. Интерфейс класса

std::stack
не позволяет делать это с помощью одного простого вызова. Однако определить лямбда-выражение нетрудно, так что теперь можно получать значения следующим образом:


double top_value {pop_stack()};


Различаем в пользовательском вводе операнды и операторы 

В основном цикле функции

evaluate_rpn
мы получаем текущий токен строки из итератора и затем смотрим, является ли он операндом. Если строка может быть преобразована в переменную типа
double
, то данное число тоже операнд. Все остальные токены, которые нельзя легко преобразовать в число (например, "
+
"), мы считаем операторами.

Скелет кода для выполнения именно этой задачи выглядит следующим образом:


stringstream ss {*it};

if (double val; ss >> val) {

  // Это число!

} else {

  // Это что-то другое. Это операция!

}


Оператор потока

>>
говорит нам, является ли рассматриваемый объект числом. Сначала мы оборачиваем строку в
std::stringstream
. Затем используем способность объекта класса
stringstream
преобразовать объект типа
std::string
в переменную типа
double
, что включает в себя разбор. Если он не работает, то мы узнаем об этом, поскольку не получится преобразовать в число некий объект, который числом не является.


Выбираем и применяем нужную математическую операцию

Разобравшись, что текущий токен, полученный от пользователя, не является числом, мы предполагаем, что это операция наподобие

+
или
*
. Затем обращаемся к ассоциативному массиву ops с целью найти требуемую операцию и получить функцию, принимающую два операнда и возвращающую сумму, произведение или другой подходящий результат.

Сам тип такого массива выглядит относительно сложно:


map ops { ... };


Он соотносит строки и значения типа

double (*)(double, double)
. Что означает вторая часть данного выражения? Это описание типа читается как «указатель на функцию, которая принимает два числа типа
double
и возвращает одно». Представьте, будто часть (
*
) представляет собой имя функции, как, например,
double sum(double, double)
, что гораздо проще прочитать. Идея заключается в следующем: наше лямбда-выражение
[](double, double) { return /* какое-то число типа double */ }
можно преобразовать в указатель на функцию, фактически соответствующий описанию этого указателя. Лямбда-выражения, которые не захватывают переменных из внешнего контекста, могут быть преобразованы в указатели на функции.

Таким образом, это удобный способ запросить у ассоциативного массива корректную операцию:


const auto & op (ops.at(*it));

const double result {op(l, r)};


Ассоциативный массив неявно решает еще одну задачу. Если мы выполняем вызов

ops.at("foo")
, то в данном случае "
foo
" является корректным значением ключа, но мы не сохранили операцию с таким именем. В подобных случаях массив сгенерирует исключение, которое мы отлавливаем в нашем примере. При его перехвате мы генерируем другое исключение, чтобы представить более подробное сообщение об ошибке. Пользователь будет лучше понимать, что означает полученное исключение, сообщающее о некорректном аргументе (
invalid argument
), в отличие от исключения, гласящего о выходе за пределы контейнера. Обратите внимание: пользователь функции
evaluate_rpn
может быть незнаком с ее реализацией и поэтому не знает о том, что мы применяем ассоциативный массив.


Дополнительная информация

Поскольку функция

evaluate_rpn
принимает итераторы, ей можно легко передавать разные входные данные, не только стандартный поток ввода. Это позволяет довольно просто протестировать ее, а также адаптировать к различным источникам данных, получаемых от пользователя.

Передача в эту функцию итераторов строкового потока или вектора строк, например, выглядит следующим образом. При этом код функции

evaluate_rpn
остается без изменений: