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
остается без изменений: