Разделение строки в C++ по разделителю (string delimiter) — как сделать split?
Правильное и эффективное решение для split в C++ (C++11/14/17)
Проблема, которую вы описали, действительно одна из самых частых у новичков в C++. Стандартная библиотека не предоставляет готовой функции split, но её легко написать самому, избежав всех перечисленных недостатков. Рассмотрим несколько подходов — от простого до самого современного (C++17).
1. Базовый вариант (C++11) — исправление вашего кода
Ваш код в целом рабочий, но его можно улучшить:
- Передавайте строку по константной ссылке (
const std::string&), чтобы избежать копирования. - Используйте
std::string::findбез удаления — это дешевле. - Добавьте флаг для пропуска пустых токенов, если нужно.
#include <iostream>
#include <string>
#include <vector>
std::vector<std::string> split(const std::string& s, const std::string& delimiter, bool skip_empty = false) {
std::vector<std::string> tokens;
size_t start = 0;
size_t end = s.find(delimiter);
while (end != std::string::npos) {
std::string token = s.substr(start, end - start);
if (!skip_empty || !token.empty()) {
tokens.push_back(token);
}
start = end + delimiter.length();
end = s.find(delimiter, start);
}
// Последний токен (после последнего разделителя)
std::string last_token = s.substr(start);
if (!skip_empty || !last_token.empty()) {
tokens.push_back(last_token);
}
return tokens;
}
int main() {
std::string str = "apple::banana::cherry";
auto tokens = split(str, "::");
for (const auto& t : tokens) {
std::cout << t << std::endl;
}
// Вывод: apple, banana, cherry
return 0;
}
2. C++17 — использование std::string_view (без копирования)
Если вы работаете с C++17 или новее, лучше использовать std::string_view — он не копирует данные, а только ссылается на них. Это особенно эффективно для больших строк.
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
std::vector<std::string_view> split(std::string_view s, std::string_view delimiter, bool skip_empty = false) {
std::vector<std::string_view> tokens;
size_t start = 0;
size_t end = s.find(delimiter);
while (end != std::string_view::npos) {
std::string_view token = s.substr(start, end - start);
if (!skip_empty || !token.empty()) {
tokens.push_back(token);
}
start = end + delimiter.length();
end = s.find(delimiter, start);
}
std::string_view last_token = s.substr(start);
if (!skip_empty || !last_token.empty()) {
tokens.push_back(last_token);
}
return tokens;
}
// Пример использования:
int main() {
std::string str = "apple::banana::cherry";
auto tokens = split(str, "::");
for (auto t : tokens) {
std::cout << t << std::endl;
}
return 0;
}
3. Альтернатива — std::getline с std::istringstream (только для одиночного символа)
Если разделитель — одиночный символ, можно использовать std::getline с потоком:
#include <sstream>
#include <string>
#include <vector>
std::vector<std::string> split_by_char(const std::string& s, char delimiter) {
std::vector<std::string> tokens;
std::istringstream stream(s);
std::string token;
while (std::getline(stream, token, delimiter)) {
tokens.push_back(token);
}
return tokens;
}
Что выбрать?
- C++11/14: используйте первый вариант (с
std::stringи ссылкой). - C++17+: используйте
std::string_view— это быстрее и современнее. - Только для одного символа:
std::getline— элегантно, но не подходит для строковых разделителей.
Ваш исходный код был близок к правильному решению. Основные улучшения: передача по ссылке, отказ от erase (дорого), добавление опции пропуска пустых токенов и использование string_view в современных версиях C++.
Альтернативный подход — итераторный split (для продвинутых)
Если вы хотите получить максимально гибкое и переиспользуемое решение, можно написать класс-итератор для разбиения строки. Это позволяет использовать range-based for и не выделять память под вектор, если токены нужны по одному.
#include <iostream>
#include <string>
#include <string_view>
#include <iterator>
class SplitIterator {
public:
SplitIterator(std::string_view s, std::string_view delim)
: str_(s), delim_(delim), pos_(0) {}
std::string_view operator*() const {
return str_.substr(pos_, next_ - pos_);
}
SplitIterator& operator++() {
pos_ = next_ + delim_.length();
next_ = str_.find(delim_, pos_);
return *this;
}
bool operator!=(const SplitIterator&) const {
return pos_ != std::string_view::npos;
}
SplitIterator begin() {
next_ = str_.find(delim_, pos_);
return *this;
}
SplitIterator end() {
SplitIterator it = *this;
it.pos_ = std::string_view::npos;
return it;
}
private:
std::string_view str_;
std::string_view delim_;
size_t pos_ = 0;
size_t next_ = 0;
};
// Пример использования:
int main() {
std::string s = "x::y::z";
for (auto token : SplitIterator(s, "::")) {
std::cout << token << std::endl;
}
return 0;
}
Этот подход:
- Не выделяет память под вектор — токены извлекаются на лету.
- Работает с
std::string_view, поэтому нет копирования. - Позволяет легко добавить фильтрацию (пропуск пустых токенов) или ленивую обработку.
Минус: больше кода, чем в простом решении. Подходит, если вы пишете библиотеку или часто используете split в проекте.