О Форматировании Строк В Современном C++

Добрый день! В этой статье я хотел бы рассказать о существующих возможностях форматирования строк в современном C++, показать свои разработки, которые я уже несколько лет использую в реальных проектах, а также сравнить производительность различных подходов к форматированию строк.

Форматирование строки — это операция, позволяющая получить результирующую строку из строки шаблона и набора аргументов.

Строка шаблона содержит текст, включающий заполнители, которые можно заменить аргументами.

Для наглядности небольшой пример:

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
   

int apples = 5; int oranges = 7; std::string str = format("I have %d apples and %d oranges, so I have %d fruits", apples, oranges, apples + oranges); std::cout << str << std::endl;

Здесь: Строка шаблона: у меня есть %d яблок и %d апельсинов, значит, у меня есть %d фруктов.

Заполнители: %d, %d, %d Аргументы: яблоки, апельсины, яблоки + апельсины.

При запуске примера мы получаем результирующую строку

I have 5 apples and 7 oranges, so I have 12 fruits

Теперь давайте посмотрим, что C++ предоставляет нам для форматирования строк.



Наследие С

Форматирование строк в C осуществляется с помощью семейства функций Xprintf. С таким же успехом мы можем использовать эти функции и в C++:

char buf[100]; int res = snprintf(buf, sizeof(buf), "I have %d apples and %d oranges, so I have %d fruits", apples, oranges, apples + oranges); std::string str = "error!"; if (res >= 0 && res < sizeof(buf)) str = buf; std::cout << str << std::endl;

Это довольно хороший способ форматирования, несмотря на кажущуюся неуклюжесть:
  • это самый быстрый способ форматирования строк
  • этот метод работает практически на всех версиях компиляторов, не требуя поддержки новых стандартов
Но, конечно, не обошлось и без недостатков:
  • нужно заранее знать, сколько памяти потребуется для полученной строки, что не всегда можно определить
  • соответствие количества и типа аргументов и заполнителей не проверяется при передаче параметров извне (как в реализованной ниже обертке над vsnprintf), что может привести к ошибкам во время выполнения программы


функция std::to_string()

Начиная с C++11, в стандартную библиотеку была добавлена функция std::to_string(), которая позволяет конвертировать переданное значение в строку.

Функция работает не со всеми типами аргументов, а только со следующими:

  • интервал
  • длинный
  • долго долго
  • неподписанный int
  • неопаленный длинный
  • беззнаковый длинный длинный
  • плавать
  • двойной
  • длинный двойной
Пример использования:

std::string str = "I have " + std::to_string(apples) + " apples and " + std::to_string(oranges) + " oranges, so I have " + std::to_string(apples + oranges) + " fruits"; std::cout << str << std::endl;



Класс std::stringstream

Класс std::stringstream — это основной способ, с помощью которого C++ предоставляет нам форматирование строк:

std::stringstream ss; ss << "I have " << apples << " apples and " << oranges << " oranges, so I have " << apples + oranges << " fruits"; std::string str = ss.str(); std::cout << str << std::endl;

Строго говоря, использование std::stringstream не является полноценным форматированием строки, поскольку вместо заполнителей мы вставляем аргументы в строку шаблона.

Это допустимо в самых простых случаях, но в более сложных существенно ухудшает читаемость кода:

ss << "A[" << i1 << ", " << j1 << "] + A[" << i2 << ", " << j2 << "] = " << A[i1][j1] + A[i2][j2];

сравнить с:

std::string str = format("A[%d, %d] + A[%d, %d] = %d", i1, j1, i2, j2, A[i1][j1] + A[i2][j2]);

Объект std::sringstream позволяет реализовать несколько интересных оберток, которые могут вам понадобиться позже.

Преобразование «чего угодно» в строку:

template<typename T> std::string to_string(const T &t) { std::stringstream ss; ss << t; return ss.str(); }



std::string str = to_string("5");

Преобразование строки в «что угодно»:

template<typename T> T from_string(const std::string &str) { std::stringstream ss(str); T t; ss >> t; return t; } template<> std::string from_string(const std::string &str) { return str; }



int x = from_string<int>("5");

Преобразование строки в «что угодно» с проверкой:

template<typename T> T from_string(const std::string &str, bool &ok) { std::stringstream ss(str); T t; ss >> t; ok = !ss.fail(); return t; } template<> std::string from_string(const std::string &str, bool &ok) { ok = true; return str; }



bool ok = false; int x = from_string<int>("x5", ok); if (!ok) .



Также можно написать пару оберток для удобного использования std::stringstream в одну строку.

Использование объекта std::stringstream для каждого аргумента:

class fstr final : public std::string { public: fstr(const std::string &str = "") { *this += str; } template<typename T> fstr &operator<<(const T &t) { *this += to_string(t); return *this; } };



std::string str = fstr() << "I have " << apples << " apples and " << oranges << " oranges, so I have " << apples + oranges << " fruits";

Использование одного объекта std::stringstream для всей строки:

class sstr final { public: sstr(const std::string &str = "") : ss_(str) { } template<typename T> sstr &operator<<(const T &t) { ss_ << t; return *this; } operator std::string() const { return ss_.str(); } private: std::stringstream ss_; };



std::string str = sstr() << "I have " << apples << " apples and " << oranges << " oranges, so I have " << apples + oranges << " fruits";

Забегая вперед, окажется, что производительность std::to_string в 3-4 раза выше, чем у to_string, реализованного с помощью std::stringstream. Поэтому логично было бы для подходящих типов использовать std::to_string, а для всех остальных использовать шаблон to_string:

std::string to_string(int x) { return std::to_string(x); } std::string to_string(unsigned int x) { return std::to_string(x); } std::string to_string(long x) { return std::to_string(x); } std::string to_string(unsigned long x) { return std::to_string(x); } std::string to_string(long long x) { return std::to_string(x); } std::string to_string(unsigned long long x) { return std::to_string(x); } std::string to_string(float x) { return std::to_string(x); } std::string to_string(double x) { return std::to_string(x); } std::string to_string(long double x) { return std::to_string(x); } std::string to_string(const char *x) { return std::string(x); } std::string to_string(const std::string &x) { return x; } template<typename T> std::string to_string(const T &t) { std::stringstream ss; ss << t; return ss.str(); }



Библиотека boost::format

Набор библиотек boost — это мощный инструмент, который прекрасно дополняет язык C++ и стандартную библиотеку.

Форматирование строк обеспечивается библиотекой boost::format. Поддерживается указание следующих элементов в качестве стандартных заполнителей:

std::string str = (boost::format("I have %d apples and %d oranges, so I have %d fruits") % apples % oranges % (apples + oranges)).

str();

и порядковые:

std::string str = (boost::format("I have %1% apples and %2% oranges, so I have %3% fruits") % apples % oranges % (apples + oranges)).

str();

Единственным недостатком boost::format является низкая производительность; это самый медленный метод форматирования строк.

Также этот метод неприменим, если в проекте нельзя использовать сторонние библиотеки.

Итак, получается, что C++ и стандартная библиотека не предоставляют нам удобных средств форматирования строк, поэтому напишем что-то свое.



Обертка над vsnprintf

Попробуем написать обертку над функцией Xprintf, выделив достаточно памяти и передав произвольное количество параметров.

Для выделения памяти мы будем использовать следующую стратегию:

  1. Сначала выделяем тот объем памяти, которого будет достаточно в большинстве случаев
  2. попробуйте вызвать функцию форматирования
  3. если вызов не удался, выделите больше памяти и повторите предыдущий шаг
Для передачи параметров мы будем использовать механизм stdarg и функцию vsnprintf.

std::string format(const char *fmt, .

) { va_list args; va_start(args, fmt); std::vector<char> v(1024); while (true) { va_list args2; va_copy(args2, args); int res = vsnprintf(v.data(), v.size(), fmt, args2); if ((res >= 0) && (res < static_cast<int>(v.size()))) { va_end(args); va_end(args2); return std::string(v.data()); } size_t size; if (res < 0) size = v.size() * 2; else size = static_cast<size_t>(res) + 1; v.clear(); v.resize(size); va_end(args2); } }



std::string str = format("I have %d apples and %d oranges, so I have %d fruits", apples, oranges, apples + oranges);

Здесь стоит уточнить пару нюансов.

Возвращаемое значение функций Xprintf зависит от платформы, на некоторых платформах она возвращает -1 в случае сбоя, и в этом случае мы удваиваем буфер.

На других платформах возвращается длина результирующей строки (без учета нулевого символа), и в этом случае мы можем сразу выделить столько памяти, сколько необходимо.

Вы можете узнать больше о поведении функций Xprintf на различных платформах.

Здесь .

Кроме того, на некоторых платформах vsnprintf() «портит» список аргументов, поэтому мы копируем его перед вызовом.

Я начал использовать эту функцию до C++11 и, с небольшими изменениями, продолжаю использовать ее и сегодня.

Основное неудобство при использовании — отсутствие поддержки std::string в качестве аргументов, поэтому нужно не забыть добавить .

c_str() ко всем строковым аргументам:

std::string country = "Great Britain"; std::string capital = "London"; std::cout << format("%s is a capital of %s", capital.c_str(), country.c_str()) << std::endl;



Вариативный шаблон

В C++, начиная с C++11, появилась возможность использовать шаблоны с переменным количеством аргументов (Variadic Templates).

Такие шаблоны можно использовать при передаче аргументов функции форматирования.

Кроме того, нам больше не нужно беспокоиться о типах аргументов, поскольку мы можем использовать шаблон to_string, реализованный ранее.

Поэтому мы будем использовать порядковые заполнители.

Чтобы получить все аргументы, мы отделяем первый аргумент, преобразуем его в строку, сохраняем и повторяем эту операцию рекурсивно.

Если аргументов нет или когда они заканчиваются (конечная точка рекурсии), мы разбираем строку шаблона, подставляем аргументы и получаем результирующую строку.

Таким образом, у нас есть все для полноценной реализации функции форматирования: парсинг строки шаблона, сбор и преобразование всех параметров в строку, подстановка параметров в строку шаблона и получение результирующей строки:

std::string vtformat_impl(const std::string &fmt, const std::vector<std::string> &strs) { static const char FORMAT_SYMBOL = '%'; std::string res; std::string buf; bool arg = false; for (int i = 0; i <= static_cast<int>(fmt.size()); ++i) { bool last = i == static_cast<int>(fmt.size()); char ch = fmt[i]; if (arg) { if (ch >= '0' && ch <= '9') { buf += ch; } else { int num = 0; if (!buf.empty() && buf.length() < 10) num = atoi(buf.c_str()); if (num >= 1 && num <= static_cast<int>(strs.size())) res += strs[num - 1]; else res += FORMAT_SYMBOL + buf; buf.clear(); if (ch != FORMAT_SYMBOL) { if (!last) res += ch; arg = false; } } } else { if (ch == FORMAT_SYMBOL) { arg = true; } else { if (!last) res += ch; } } } return res; } template<typename Arg, typename .

Args> inline std::string vtformat_impl(const std::string& fmt, std::vector<std::string>& strs, Arg&& arg, Args&& .

args) { strs.push_back(to_string(std::forward<Arg>(arg))); return vtformat_impl(fmt, strs, std::forward<Args>(args) .

); } inline std::string vtformat(const std::string& fmt) { return fmt; } template<typename Arg, typename .

Args> inline std::string vtformat(const std::string& fmt, Arg&& arg, Args&& .

args) { std::vector<std::string> strs; return vtformat_impl(fmt, strs, std::forward<Arg>(arg), std::forward<Args>(args) .

); }

Алгоритм оказался весьма эффективным; он работает за один проход по строке формата.

Если аргумент не может быть вставлен вместо заполнителя, он остается неизменным и исключения не создаются.

Примеры использования:

std::cout << vtformat("I have %1 apples and %2 oranges, so I have %3 fruits", apples, oranges, apples + oranges) << std::endl; I have 5 apples and 7 oranges, so I have 12 fruits std::cout << vtformat("%1 + %2 = %3", 2, 3, 2 + 3) << std::endl; 2 + 3 = 5 std::cout << vtformat("%3 = %2 + %1", 2, 3, 2 + 3) << std::endl; 5 = 3 + 2 std::cout << vtformat("%2 = %1 + %1 + %1", 2, 2 + 2 + 2) << std::endl; 6 = 2 + 2 + 2 std::cout << vtformat("%0 %1 %2 %3 %4 %5", 1, 2, 3, 4) << std::endl; %0 1 2 3 4 %5 std::cout << vtformat("%1 + 1% = %2", 54, 54 * 1.01) << std::endl; 54 + 1% = 54.540000 std::string country = "Russia"; const char *capital = "Moscow"; std::cout << vtformat("%1 is a capital of %2", capital, country) << std::endl; Moscow is a capital of Russia template<typename T> std::ostream &operator<<(std::ostream &os, const std::vector<T> &v) { os << "["; bool first = true; for (const auto &x : v) { if (first) first = false; else os << ", "; os << x; } os << "]"; return os; } std::vector<int> v = {1, 4, 5, 2, 7, 9}; std::cout << vtformat("v = %1", v) << std::endl; v = [1, 4, 5, 2, 7, 9]



Сравнение производительности

Сравнение производительности to_string и std::to_string, миллисекунд на миллион вызовов
интервал, мс долго-долго, мс двойной, мс
нанизывать 681 704 1109
std::to_string 130 201 291


О форматировании строк в современном C++

Сравнение производительности функции форматирования, миллисекунд на миллион вызовов
РС
fstr 1308
сстр 1243
формат 788
повышение::формат 2554
vtformat 2022


О форматировании строк в современном C++

Спасибо за внимание.

Комментарии и дополнения приветствуются.

Теги: #форматирование строки #формат строки #to_string #stringstream #boost::format #C++

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.