В ответе max66 указана причина, по которой у вашего метода возникла проблема со строкой форматирования, и способы ее устранения. По сути, вам просто нужен какой-то способ выбора другой строки формата в зависимости от типа форматируемого значения.
Однако я хотел бы указать на еще один недостаток: вы предполагаете, что для любого данного значения потребуется только 9 символы для преобразования в строку. Для очень больших значений (например, 1e22
) это не удастся. G CC фактически выдаст вам предупреждение, если сможет определить это во время компиляции.
Кроме того, ваша текущая реализация выделяет много строк и рекурсивно добавляет их вместе. Это, конечно, крайне неэффективно и снижает скорость семейства функций printf
до такой степени, что их не стоит использовать на самом деле.
Также ваше решение не проверяет ошибки формата (snprintf()
возвращает отрицательный в этом случае). И в таких случаях вы можете добавить неопределенную память к вашей строке, поскольку я не уверен, что стандарт C гарантирует нулевое завершение буфера при сбое (но это может быть) .
Мое решение состоит в том, чтобы иметь функцию, которая форматирует данный аргумент на месте в конце std::string
. Кроме того, он обрабатывает ошибки форматирования и случаи, когда 9 байтов недостаточно для хранения отформатированного значения.
Кроме того, я накладываю ограничения SFINAE на типы аргументов, чтобы гарантировать, что он может вызываться только с поддерживаемыми нами типами.
Вот мое решение с комментариями, чтобы объяснить, что делает, что и почему:
#include <string>
#include <type_traits>
#include <iostream>
// checks if T is a type we support
template<typename T>
inline constexpr bool allowed_type = std::is_floating_point_v<T> || std::is_integral_v<T>;
// the initial amount of space for stringifying each argument
constexpr std::size_t APPEND_PADDING = 20;
// returns the appropriate format string for type T (T assumed to be supported)
template<typename T>
const char *fmt_string()
{
if constexpr (std::is_floating_point_v<T>) return "%f";
else return "%d";
}
// stringifys val onto the end of str (T assumed to be supported)
template<typename T>
void append(std::string &str, T val)
{
std::size_t prev_size = str.size(); // remember the previous size of str
str.resize(prev_size + APPEND_PADDING); // allocate the space we need
const char *fmt = fmt_string<T>(); // get the format string to use
// format the value and check the save the return value
int res = snprintf(&str[prev_size], APPEND_PADDING, fmt, val);
// on format error, just skip it (or )
if (res < 0) str.resize(prev_size);
// if we didn't have enough room we need to try again with the correct size
if ((std::size_t)res >= APPEND_PADDING)
{
str.resize(prev_size + res + 1); // make space for the characters we need and the null terminator
snprintf(&str[prev_size], res + 1, fmt, val); // format the string again (this time will work)
str.pop_back(); // remove the null terminator
}
// otherwise we had enough room, so just truncate to the written characters
else str.resize(prev_size + res);
}
// formats all of args into a single string (only allows supported types)
template<typename ...Args, std::enable_if_t<(allowed_type<Args> && ...), int> = 0>
std::string format(Args ...args)
{
std::string str; // create an empty buffer string to store the result
str.reserve(sizeof...(args) * APPEND_PADDING); // predict how much space we'll need for everything
int _[] = { (append(str, args), 0)... }; // append all the args to str one at a time
(void)_; // suppress unused variable warnings (will just be optimized away)
return str;
}
int main()
{
std::cout << format(1, 2, 2.3, 3, 4.4, 5, 1e22) << '\n';
}
Обратите внимание, что при этом все отформатированные строки выполняются без разделения. Исправить это было бы так же просто, как изменить строки формата, возвращаемые с fmt_string()
.
Я использовал имена других функций, чем вы, но вы поняли идею. format()
- это функция, которую вы бы использовали.