Avatar ·

Разделение строки в C++ по разделителю (string delimiter) — как сделать split?

Привет всем! Я только начал изучать C++ и столкнулся с задачей, которая в других языках решается одной функцией. Мне нужно разбить строку на части по разделителю (например, по символу запятой или по подстроке "::"). В Python я пишу `"a,b,c".split(",")` и получаю список. В C++ я не могу найти ничего подобного в стандартной библиотеке. Есть `strtok` из C, но он работает с `char*`, а мне нужно работать с `std::string`. Кроме того, `strtok` модифицирует исходную строку и не является потокобезопасным. Я пробовал писать свой велосипед, но он работает криво: ```cpp #include #include std::vector split(std::string s, std::string delimiter) { size_t pos = 0; std::vector tokens; while ((pos = s.find(delimiter)) != std::string::npos) { std::string token = s.substr(0, pos); tokens.push_back(token); s.erase(0, pos + delimiter.length()); } tokens.push_back(s); return tokens; } ``` Проблемы: 1. Если разделитель состоит из нескольких символов (например, "::"), то в конце могут оставаться пустые строки. 2. Если строка начинается с разделителя, то первым токеном будет пустая строка — это не всегда нужно. 3. Код копирует строку много раз, что неэффективно. Подскажите, как правильно и эффективно разбить строку в C++ на токены по строковому разделителю? Нужно именно стандартное решение без Boost и сторонних библиотек. Желательно с примерами для C++11/14/17. Заранее спасибо! <<>>

Правильное и эффективное решение для 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 в проекте.