Различаем в пользовательском вводе операнды и операторы
В основном цикле функции evaluate_rpn
мы получаем текущий токен строки из итератора и затем смотрим, является ли он операндом. Если строка может быть преобразована в переменную типа double
, то данное число тоже операнд. Все остальные токены, которые нельзя легко преобразовать в число (например, "+
"), мы считаем операторами.
Скелет кода для выполнения именно этой задачи выглядит следующим образом:
stringstream ss {*it};
if (double val; ss >> val) {
// Это число!
} else {
// Это что-то другое. Это операция!
}
Оператор потока >>
говорит нам, является ли рассматриваемый объект числом. Сначала мы оборачиваем строку в std::stringstream
. Затем используем способность объекта класса stringstream
преобразовать объект типа std::string
в переменную типа double
, что включает в себя разбор. Если он не работает, то мы узнаем об этом, поскольку не получится преобразовать в число некий объект, который числом не является.
Выбираем и применяем нужную математическую операцию
Разобравшись, что текущий токен, полученный от пользователя, не является числом, мы предполагаем, что это операция наподобие +
или *
. Затем обращаемся к ассоциативному массиву ops с целью найти требуемую операцию и получить функцию, принимающую два операнда и возвращающую сумму, произведение или другой подходящий результат.
Сам тип такого массива выглядит относительно сложно:
map<string, double (*)(double, double)> 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
остается без изменений:
int main()
{
stringstream s {"3 2 1 + * 2 /"};
cout << evaluate_rpn(istream_iterator<string>{s}, {}) << '\n';
vector<string> v {"3", "2", "1", "+", "*", "2", "/"};
cout << evaluate_rpn(begin(v), end(v)) << '\n';
}
Используйте итераторы везде, где это имеет смысл. Так вы сможете многократно применять свой код.
Подсчитываем частоту встречаемости слов с применением контейнера std::map
Контейнер std::map
очень полезен в тех случаях, когда нужно разбить данные на категории и собрать соответствующую статистику. Прикрепляя изменяемые объекты к каждому ключу, который представляет собой категорию объектов, вы легко можете реализовать, к примеру, гистограмму, показывающую частоту встречаемости слов. Этим мы и займемся.