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
Он соотносит строки и значения типа double (*)(double, double)
. Что означает вторая часть данного выражения? Это описание типа читается как «указатель на функцию, которая принимает два числа типа double
и возвращает одно». Представьте, будто часть (*
) представляет собой имя функции, как, например, double sum(double, double)
, что гораздо проще прочитать. Идея заключается в следующем: наше лямбда-выражение [](double, double) { return /* какое-то число типа double */ }
можно преобразовать в указатель на функцию, фактически соответствующий описанию этого указателя. Лямбда-выражения, которые
Таким образом, это удобный способ запросить у ассоциативного массива корректную операцию:
const auto & op (ops.at(*it));
const double result {op(l, r)};