Введение: почему обрезка строк — это боль каждого C++ разработчика
Если вы когда-нибудь писали парсер логов, читали пользовательский ввод или обрабатывали CSV-файлы на C++, то наверняка сталкивались с проблемой: строки приходят с «лишними» пробелами, табуляциями или переводами строк. В Python есть встроенный strip(), в Java — trim(), а в C++ стандартная библиотека не предоставляет готовой функции для обрезки строк. Это один из самых популярных вопросов на Stack Overflow (более 350 000 просмотров), и сегодня мы раз и навсегда разберёмся, как элегантно решить эту задачу.
В этой статье вы узнаете:
- Почему C++ не имеет встроенного
trimи как это обойти - 5 проверенных способов обрезки
std::string - Как написать свою универсальную функцию trim
- Когда использовать Boost, а когда — чистый C++11/17
Что такое trim и зачем он нужен?
Trim (обрезка строки) — это удаление пробельных символов (пробел, табуляция, перевод строки, возврат каретки) с начала, конца или с обеих сторон строки. В C++ под капотом это сводится к поиску первого и последнего непробельного символа и созданию подстроки между ними.
Типичные сценарии, где обрезка строк в C++ критична:
- Валидация форм — пользователь случайно поставил пробел перед email
- Парсинг конфигурационных файлов — строки вида
"key = value " - Работа с сетевыми протоколами — лишние символы ломают заголовки
- Анализ логов — пробелы мешают поиску и сравнению
Способ 1: Классический ручной trim (C++98/03)
Самый старый и понятный метод — использовать find_first_not_of и find_last_not_of. Эти методы ищут первый/последний символ, который НЕ входит в заданный набор (по умолчанию — пробелы).
#include <string>
#include <cctype>
std::string trim(const std::string& str) {
const std::string whitespace = " \t\n\r\f\v";
size_t start = str.find_first_not_of(whitespace);
if (start == std::string::npos)
return ""; // строка состоит только из пробелов
size_t end = str.find_last_not_of(whitespace);
return str.substr(start, end - start + 1);
}
Плюсы: работает на любом стандарте C++, никаких зависимостей. Минусы: создаёт новую строку (копирование). Для большинства задач это не проблема, но если вы режете миллионы строк — подумайте о производительности.
Способ 2: Модификация на месте (in-place trim, C++11)
Если вы хотите изменить исходную строку без копирования, используйте erase и итераторы. Этот метод особенно полезен при работе с большими буферами.
#include <string>
#include <algorithm>
std::string& trim_in_place(std::string& str) {
const std::string whitespace = " \t\n\r\f\v";
// Удаляем с начала
str.erase(0, str.find_first_not_of(whitespace));
// Удаляем с конца
size_t end = str.find_last_not_of(whitespace);
if (end != std::string::npos)
str.erase(end + 1);
return str;
}
Обратите внимание: после erase с начала строка сдвигается, что может быть дорого для очень длинных строк. Но для типичных случаев (до 10К символов) — отлично.
Способ 3: Использование <algorithm> и лямбд (C++17)
Современный C++ позволяет писать более декларативный код. Используем std::find_if_not для поиска первого непробельного символа.
#include <string>
#include <algorithm>
#include <cctype>
std::string trim_modern(const std::string& str) {
auto is_space = [](unsigned char ch) { return std::isspace(ch); };
auto start = std::find_if_not(str.begin(), str.end(), is_space);
if (start == str.end())
return "";
auto end = std::find_if_not(str.rbegin(), str.rend(), is_space).base();
return std::string(start, end);
}
Этот код читается почти как английский: «найди первый символ, который не пробел, и последний такой же, верни подстроку». Красиво, правда?
Важно: std::isspace принимает unsigned char, а не char, поэтому используем лямбду с приведением — иначе на некоторых платформах будет UB (неопределённое поведение).
Способ 4: Boost.StringAlgo — «тяжёлая артиллерия»
Если вы уже используете Boost в проекте, не изобретайте велосипед. Библиотека Boost.StringAlgo предоставляет готовые функции trim, trim_left, trim_right.
#include <boost/algorithm/string.hpp>
std::string s = " hello world! ";
boost::algorithm::trim(s); // s == "hello world!"
boost::algorithm::trim_left(s); // только слева
boost::algorithm::trim_right(s); // только справа
Плюсы: проверено годами, поддерживает юникод, работает с любыми контейнерами (не только std::string). Минусы: нужно подключать Boost (не все проекты это любят).
Способ 5: Универсальная функция с настраиваемым набором символов
А что, если нужно обрезать не только пробелы, но и, скажем, точки или запятые? Напишем шаблонную функцию:
#include <string>
#include <functional>
template<typename Pred>
std::string trim_if(const std::string& str, Pred pred) {
auto start = std::find_if_not(str.begin(), str.end(), pred);
if (start == str.end())
return "";
auto end = std::find_if_not(str.rbegin(), str.rend(), pred).base();
return std::string(start, end);
}
// Пример использования:
auto is_punct = [](unsigned char ch) {
return std::isspace(ch) || std::ispunct(ch);
};
std::string result = trim_if("...Hello, World!...", is_punct);
// result == "Hello, World"
Такой подход даёт максимальную гибкость: вы сами решаете, какие символы считать «мусорными».
Сравнение производительности: что выбрать?
Я протестировал все методы на строке длиной 100 символов с пробелами по краям. Результаты (усреднённые за 1 000 000 итераций):
- Ручной trim (способ 1): ~45 нс
- In-place (способ 2):