Я пишу сравнение простой программы подсчета слов на разных языках. Задача состоит в том, чтобы подсчитать частоту появления уникальных слов, разделенных пробелами, во входных данных и сначала вывести наиболее частые результаты. Регистр должен быть приведен к нижнему регистру (можно использовать ASCII).
Но прошло много времени с тех пор, как я написал C ++ (до C ++ 11 дней). Это идиоматический современный C ++? Любые улучшения, которые я мог бы сделать, чтобы сделать это более идиоматичным или более простым (я не ищу здесь повышения эффективности). Я хочу придерживаться стандартной библиотеки.
#include <algorithm>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
using namespace std;
int main() {
string word;
unordered_map<string, int> counts;
while (cin >> word) {
transform(word.begin(), word.end(), word.begin(),
[](unsigned char c){ return tolower(c); });
counts[word]++;
}
vector<pair<string, int>> ordered(counts.begin(), counts.end());
sort(ordered.begin(), ordered.end(), [](auto &a, auto &b) {
return a.second > b.second;
});
for (auto count : ordered) {
cout << count.first << " " << count.second << "n";
}
return 0;
}
3 ответа
В основном это выглядит нормально.
using namespace std;
это плохая привычка из-за возможных конфликтов имен. Вместо этого предпочитаю вводитьstd::
где необходимо.counts[word]++;
. Нам не нужна неинкрементная копия счетчика, поэтому вместо этого мы должны использовать предварительное приращение:++counts[word];
sort(ordered.begin(), ordered.end(), [](auto &a, auto &b)
мы не меняемa
или жеb
в лямбде, поэтому они должны бытьconst&
.for (auto count : ordered)
копирует пары. Опять же, мы должны использоватьauto const&
здесь.return 0;
нам это не нужно, так как компилятор добавит его автоматически. Мы также можем использовать именованную константу вместо0
, конкретноEXIT_SUCCESS
из<cstdlib>
.
Ответ user673679 касается обзора низкого уровня; Посмотрю алгоритм.
Во-первых, высшие оценки за избежание наиболее частой ошибки с функциями из <cctype>
— жизненно важно передать значение как unsigned char
повышен до int, а не просто char
.
Вместо того, чтобы писать собственный цикл для чтения слов, я бы рассмотрел transform()
от итератора входного потока до модуля вставки карты. Тем не менее, нам нужно будет создать собственный итератор для нужного нам средства вставки. Было бы очень просто, если бы мы использовали std::multiset
, но для этого потребуется больше памяти (поскольку в нем хранятся все добавленные объекты). Мы могли бы создать класс «счетчик» с соответствующими push_back()
, если бы мы могли использовать его снова.
После while
цикл, мы должны протестировать std::cin.bad()
(или настройте поток на выдачу ошибки). В настоящее время любая ошибка потока игнорируется, и мы действуем так, как если бы мы прочитали весь ввод, давая вводящие в заблуждение результаты.
Вместо того, чтобы заполнять вектор и впоследствии сортировать его, мы могли бы предпочесть вставить непосредственно в упорядоченный контейнер (std::set
возможно).
Что-то, что мы можем сделать в современном C ++, — это написать вложенные функции, присвоив переменной лямбда-выражение. Это может быть проще, чем писать встроенную лямбду. Или мы можем использовать анонимное пространство имен для функций со статической связью.
Вот моя модификация кода, чтобы не было явных циклов или каких-либо инструкций управления потоком:
#include <algorithm>
#include <cctype>
#include <iostream>
#include <iterator>
#include <set>
#include <string>
#include <unordered_map>
#include <utility>
namespace {
auto downcase(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c){ return std::tolower(c); });
return s;
}
}
int main()
{
using counter = std::unordered_map<std::string, unsigned>;
using in_it = std::istream_iterator<std::string>;
using out_it = std::ostream_iterator<std::string>;
counter counts;
auto insert = [&](std::string s) { ++counts[downcase(std::move(s))]; };
// read words into counter
std::cin.exceptions(std::istream::badbit);
std::for_each(in_it{std::cin}, in_it{}, insert);
// sort by frequency, then alphabetical
auto by_freq_then_alpha = [](const auto &a, const auto &b) {
return std::pair{ b.second, a.first } < std::pair{ a.second, b.first};
};
const std::set ordered{counts.begin(), counts.end(), by_freq_then_alpha};
// write the output
auto format = [](const auto& count) {
return count.first + ' ' + std::to_string(count.second) + 'n';
};
std::transform(ordered.begin(), ordered.end(), out_it{std::cout}, format);
}
Для более современного подхода C ++ 20 включает библиотеку Ranges, которая позволяет нам использовать представления для преобразования коллекций:
int main()
{
using counter = std::unordered_map<std::string, unsigned>;
counter counts;
auto insert = [&counts](std::string s) { ++counts[downcase(std::move(s))]; };
// read words into counter
std::cin.exceptions(std::istream::badbit);
auto input_words = std::ranges::istream_view<std::string>(std::cin);
std::ranges::for_each(input_words, insert);
// sort by frequency, then alphabetical
auto by_freq_then_alpha = [](const auto &a, const auto &b) {
return std::pair{ b.second, a.first } < std::pair{ a.second, b.first};
};
const std::set ordered{counts.begin(), counts.end(), by_freq_then_alpha};
// write the output
auto write_out = [](const auto& count) {
std::cout << count.first << ' ' << count.second << 'n';
};
std::ranges::for_each(ordered, write_out);
}
Я не говорю, что это обязательно то, что вы должен записывать; по крайней мере, на данный момент циклы на основе диапазонов более знакомы большинству программистов на C ++ и поэтому, вероятно, наиболее понятны. Однако он демонстрирует некоторые возможности современного C ++.
- 3
Хотя правильный и очень идиоматичный C ++, я на самом деле немного предпочитаю код OP для удобочитаемости, он немного короче и его легче сканировать, но опять же, у меня есть 20-летняя привычка / опыт чтения для циклов, и они просто читают яснее, чем transform / for_each и компания из . Но у них есть свое место.
— Эмили Л.
Что ж, это было образование, спасибо! Определенно совершенно другой, более функциональный подход. Я предпочитаю более простой подход с использованием цикла for (возможно, потому, что он напоминает мне Scala, к которому у меня нет теплых нечетких чувств). И спасибо за подсказку об обработке ошибок.
— Бен Хойт
Добавлено: структурированные привязки
Изменено: на использование алгоритмов на основе диапазона. (C ++ 20) Это устраняет необходимость в begin () / end (), и вы можете использовать проекцию в вызове сортировки.
Удалено: лямбды — не нужны
#include <algorithm>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
using namespace std;
int main()
{
unordered_map<string, int> counts;
string word;
while (cin >> word) {
ranges::transform(word, word.begin(), ::tolower);
counts[word]++;
}
using word_count = pair<string, int>;
vector<word_count> ordered(counts.begin(), counts.end());
ranges::sort(ordered, greater{}, &word_count::second);
for (auto [word, count] : ordered) {
cout << word << " " << count << "n";
}
}
Лямбда-упаковка std::tolower
очень определенно необходимо — используя <cctype>
функции, напрямую продвигая свои аргументы из простых char
к int
ведет к UB на платформах, где char
подписано. Всегда конвертировать в unsigned char
перед преобразованием в int
.
— Тоби Спейт
Вы пропустили невозможность включить
<cctype>
, необходим дляstd::tolower()
.— Тоби Спейт
Спасибо — оцените эти настройки!
— Бен Хойт