Разбираем строку на атомы: как пройтись по словам в C++ (и не сломать мозг)
Представьте: вы пишете анализатор логов, парсер команд или просто хотите подсчитать слова в предложении. Первое, что приходит в голову — взять строку и пройтись по каждому слову отдельно. В Python это делается в одну строчку: "hello world".split(). Но мы в C++, а значит, простота — это роскошь, которую нужно заслужить.
Вопрос "How do I iterate over the words of a string?" на Stack Overflow собрал сотни тысяч просмотров. И это не случайно: новички тонут в нюансах работы с памятью, а профи ищут баланс между скоростью и читаемостью. В этой статье мы разберём 5 способов итерации по словам в C++, сравним их производительность и поймём, когда какой использовать.
Если вы когда-нибудь писали strtok и получали segfault, или пытались вставить std::regex в горячий цикл — эта статья для вас.
Способ №1: Классика — std::istringstream
Самый безопасный и интуитивный способ для новичков. std::istringstream работает как поток: вы просто читаете из него слова, разделённые пробелами.
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
int main() {
std::string text = "C++ is not for the faint of heart";
std::istringstream stream(text);
std::string word;
std::vector<std::string> words;
while (stream >> word) {
words.push_back(word);
std::cout << "Word: " << word << std::endl;
}
std::cout << "Total words: " << words.size() << std::endl;
return 0;
}
Плюсы: чистый код, никаких сырых указателей, автоматическое управление памятью. Работает с любыми разделителями-пробелами (табуляция, перевод строки).
Минусы: медленнее ручных методов из-за создания потока. Не подходит для кастомных разделителей (только пробельные символы).
Когда использовать: когда читаемость важнее скорости. Идеально для учебных примеров и прототипов.
Способ №2: Быстрый и опасный — std::strtok
Наследие из C, которое до сих пор живёт в продакшене. strtok модифицирует исходную строку (вставляет нулевые символы) и использует статический буфер.
#include <iostream>
#include <cstring>
#include <vector>
int main() {
char text[] = "Fast but dangerous — handle with care";
std::vector<char*> words;
char* token = std::strtok(text, " ");
while (token != nullptr) {
words.push_back(token);
std::cout << "Word: " << token << std::endl;
token = std::strtok(nullptr, " ");
}
std::cout << "Total words: " << words.size() << std::endl;
return 0;
}
Плюсы: очень быстрый (работает на уровне C-строк). Позволяет задать любой набор разделителей.
Минусы: не thread-safe, разрушает исходную строку, работает только с char*, а не std::string. Легко получить undefined behavior.
Когда использовать: только в горячих циклах, где каждая наносекунда на счету, и вы уверены в однопоточности. В современном C++ лучше избегать.
Способ №3: Ручной перебор — std::string::find и substr
Иногда нужно полное control. Ручной перебор даёт гибкость и не требует дополнительных библиотек.
#include <iostream>
#include <string>
#include <vector>
std::vector<std::string> splitManual(const std::string& text, char delimiter = ' ') {
std::vector<std::string> tokens;
size_t start = 0;
size_t end = text.find(delimiter);
while (end != std::string::npos) {
tokens.push_back(text.substr(start, end - start));
start = end + 1;
end = text.find(delimiter, start);
}
tokens.push_back(text.substr(start));
return tokens;
}
int main() {
std::string text = "Manual control gives you power";
auto words = splitManual(text);
for (const auto& word : words) {
std::cout << "Word: " << word << std::endl;
}
return 0;
}
Плюсы: полный контроль над процессом, можно обрабатывать пустые токены, легко адаптировать под сложные разделители.
Минусы: много кода, легко ошибиться с граничными случаями (пустая строка, несколько разделителей подряд).
Когда использовать: когда нужен кастомный парсинг, например, разбор CSV или сложных форматов.
Способ №4: Современный и мощный — std::regex и std::sregex_token_iterator
Регулярные выражения — это швейцарский нож для работы со строками. В C++11 появилась полноценная поддержка regex, и теперь можно разбивать строки буквально одной командой.
#include <iostream>
#include <regex>
#include <string>
#include <vector>
int main() {
std::string text = "Regex: powerful but slow";
std::regex word_regex(R"(\w+)"); // Только буквенно-цифровые символы
std::vector<std::string> words;
auto words_begin = std::sregex_iterator(text.begin(), text.end(), word_regex);
auto words_end = std::sregex_iterator();
for (auto it = words_begin; it != words_end; ++it) {
words.push_back(it->str());
std::cout << "Word: " << it->str() << std::endl;
}
std::cout << "Total words: " << words.size() << std::endl;
return 0;
}
Плюсы: невероятная гибкость — можно игнорировать знаки препинания, выделять только цифры, обрабатывать Unicode. Читаемый код для сложных паттернов.
Минусы: медленный (компиляция regex — дорогая операция). Не подходит для горячих путей.
Когда использовать: когда сложность паттерна оправдывает затраты. Отличный выбор для конфигурационных файлов, логов, текстовой аналитики.
Способ №5: Boost — boost::split и boost::tokenizer
Если вы уже используете Boost (а это половина C++ сообщества), то