if (double val; ss >> val) {
val_stack.push(val);
}
9. Если же операция завершается неудачно, то перед нами нечто отличное от оператора. Это может быть только операнд. Зная, что все поддерживаемые нами операции бинарны, нужно вытолкнуть два последних операнда из стека:
else {
const auto r {pop_stack()};
const auto l {pop_stack()};
10. Теперь мы получаем операнд путем разыменования итератора it
, который возвращает строки. Обратившись в ассоциативный массив 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<string>{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()};