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


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


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


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;

Здесь:
Строка-шаблон: I have %d apples and %d oranges, so I have %d fruits
Местозаполнители: %d, %d, %d
Аргументы: apples, oranges, apples + oranges


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


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

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


Наследие 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
  • long
  • long long
  • unsinged int
  • unsinged long
  • unsigned long long
  • float
  • double
  • long double

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


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);
    }
}

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;

Шаблон с переменным количеством аргументов (Variadic Template)


В 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> std::string vtformat_impl(const std::string &fmt, std::vector<std::string> &strs, Arg arg, Args ... args)
{
    strs.push_back(to_string(arg));
    return vtformat_impl(fmt, strs, args ...);
}

std::string vtformat(const std::string &fmt)
{
    return fmt;
}

template<typename Arg, typename ... Args> std::string vtformat(const std::string &fmt, Arg arg, Args ... args)
{
    std::vector<std::string> strs;
    return vtformat_impl(fmt, strs, arg, 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, миллисекунд на миллион вызовов


int, мс long long, мс double, мс
to_string 681 704 1109
std::to_string 130 201 291

image


Сравнение производительности функций форматирования, миллисекунд на миллион вызовов


мс
fstr 1308
sstr 1243
format 788
boost::format 2554
vtformat 2022

image


Спасибо за внимание. Замечания и дополнения приветствуются.

Поделиться с друзьями
-->

Комментарии (54)


  1. Satus
    07.01.2017 05:36
    +3

    Было бы интереснее взглянуть на реализацию форматирования строк на шаблонах и constexpr, которая будет сама переключаться между compile-time и runtime форматированием когда нужно.


    1. TargetSan
      07.01.2017 15:52

      Насколько я знаю, нет возможности превратить строковой литерал в параметр типа чтобы распарсить и проверить его в компайл-тайме. Точнее, вроде как можно — но на практике оказывается что нельзя.


      1. DistortNeo
        07.01.2017 18:29

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


        Пробовал, кстати, сам написать велосипед — чисто для практики с variadic templates, только форматирование хотел сделать в стиле C#: my::string::Format("Hello, {0}! You are {1:0.00}", "Vasja", 12.0f);


        Если вкратце: Args... преобразовывался в std::tuple<StringWriter<Args...>>, по которому затем строился массив (StringWriterBase*)[N], к которому и происходило обращение по индексу аргумента в стиле arr[index]->Write(target_stringbuilder, format_string_slice);


        Велосипед, как ни странно, работал, но вот реализовывать поддержку всех опций форматирования строк как в .NET не было ни малейшего желания.


      1. Antervis
        09.01.2017 11:25

        там немного в другом проблема. constexpr не умеет работать с кучей, а сделать constexpr string конструктор через small string optimization невозможно т.к. в языке нет возможности перегружать функции по длине и/или статичности строкового литерала


  1. cranium256
    07.01.2017 08:21
    +2

    В наследии C самая крутая фишка не в том, что значения аргументов вставляются в указанные места строки формата (хотя это тоже, как вы правильно отметили, немаловажно). А в том, что эти значения форматируются в соответствии с указанными спецификациями.

    На сколько я вижу, подобной функциональностью обладает только вариант «Обертка над vsnprintf». Но в нём нет контроля соответствия спецификации формата и типа аргумента.


    1. maaGames
      07.01.2017 11:08

      Насколько помню, boost поддерживает спецификацию типа. По крайней мере, когда я писал аналог буста, то мне удалось сделать и спецификаторы типа и порядковые номера одновременно. Делал ради фана, ни в одном проекте так и не использовал библиотеку. Про производительность тоже ничего сказать не могу, скорее всего не сильно быстро.


    1. yiselieren
      07.01.2017 17:11
      +1

      По поводу контроля над спецификациями формата и типа аргумента:
      Добавляем __attribute__ ((format (printf, N, M)))
      после прототипа функции.


  1. demp
    07.01.2017 12:37
    +9

    Зачем использовать boost.format, если достаточно давно есть fmt, ранее известная как cppformat.


    И у любителей compile-time недавно появилось поле для экспериментов: pprintpp


    При написании своего велосипеда полезно понимать, чем он лучше остальных. Для сравнения производительности с другими можно сравнить результаты из format-benchmark.


    1. 5nw
      07.01.2017 17:16

      Про эту библиотеку ничего не знал, спасибо, посмотрю на досуге


    1. mikeus
      07.01.2017 17:46

      И у любителей compile-time недавно появилось поле для экспериментов: pprintpp
      А это прикольно. Но взглянув на этот фрагмент, можно понять что, если в примере использования:
      #include <pprintpp.hpp>
      
      #include <cstdio>
      
      int main()
      {
          pprintf("{} hello {s}! {}\n", 1, "world", 2.0) ;
      }
      
      заменить хотя бы на:
      #include <pprintpp.hpp>
      
      #include <cstdio>
      
      int main()
      {
          const char *f = "{} hello {s}! {}\n";
          pprintf(f, 1, "world", 2.0) ;
      }
      
      то все посыпется…


      1. demp
        07.01.2017 20:56

        Похоже маловато константности, может constexpr поможет.


  1. ilynxy
    07.01.2017 12:56

    Зачем использовать boost.format, если достаточно давно есть fmt, ранее известная как cppformat.

    Как у fmt сейчас с локалями? Поддерживает? Я не слежу, но совсем недавно была печаль.


    1. demp
      07.01.2017 20:53

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


      Вообще, локали одно из самых тормозных мест в стандартной библиотеке, и в плане компиляции, и времени выполнения. Так что автора cppformat можно понять, что он не хочет с ними связываться.


    1. monostate
      08.01.2017 22:48

      Локали поддерживаются как в питоне через спецификатор форматирования n (http://fmtlib.net/3.0.0/syntax.html#format-specification-mini-language).


      Форматирование по умолчанию не зависит от локали по причинам указанным в http://fmtlib.net/Text%20Formatting.html#Locale


  1. forester11
    07.01.2017 13:16
    -2

    А чем плох snprintf?
    Быстрее и компактней ничего наверно нет, компиляторы выдают предупреждения на несоответствие типов. Нельзя определить точно размер буфера? Да и ненадо, на стеке выделить 256/512/..., а организацию вызовов (интерфейсы) можно организовать так что свободно летающие std::string не требуются. Причем на стеке память выделяется за константное (бесплатное) время а не поиск в хипе свободного блока по хитрому алгоритму с синхронизацией тредов.
    Т.е. можно вообще не тратить время на изобретение новых классов, использовать vsnprintf, и выпилить в дизайне необходимость в свободно летающих std::string (часто форматирование строк сугубо задача UI модуля, вот в нем и можно скрыть эти заморочки прокинув стековые строки прямо в native API UI библиотеки). Т.е. тот же printf печатает прямо в стандартный вывод без копирования строк, а некоторые библиотеки дают возможность делать sprintf прямо в UI контрол. Тогда зачем нам временный std::string? Так, построить код ради кода.


    1. snizovtsev
      07.01.2017 14:25
      +3

      Лишь первое, что пришло в голову:


      1. Нельзя использовать нестандартные типы. Особенно раздражает с std::string и string_view;
      2. Если мне не изменяет память — сишный formatstring непортабелен (%d, %ld, %lld). Отсюда вырвиглазные макросы аля PRIi64;
      3. Назначение — далеко не только UI. Еще логирование и отладка как минимум.


      1. staticlab
        07.01.2017 17:40

        Ещё в C++ из коробки поддерживается форматирование чисел в соответствии с локалью.


  1. win32asm
    07.01.2017 13:21
    +1

    Александреску давно возмущался несовершенством мира вообще и форматирования строк в С++ в частности — https://erdani.com/publications/cuj-2005-08.pdf (ссылки на код в статье битые 8-( )
    Учитывая что воз и ныне там, кажется, на эту фичу слегка подзабили. Правда я в С++14/17 не вчитывался на эту тему…


    1. 5nw
      07.01.2017 18:20

      Интересная статья, спасибо


  1. Rend
    07.01.2017 13:35
    +5

    В случае обёртки над vsnprintf вместо многочисленных попыток выделить память можно просто спросить у функции, сколько же ей нужно.

    string string_format(const char* format, ...)
    {
        va_list args;
        va_start(args, format);
    
        int buf_len = vsnprintf(nullptr, 0, format, args);
        unique_ptr<char[]> strBuf(new char[buf_len + 1]);
        vsnprintf(strBuf.get(), buf_len + 1, format, args);
    
        return string(strBuf.get());
    }
    


    1. Effolkronium
      07.01.2017 17:12
      +1

      Лучше так (14 стандарт):

      auto strBuf = std::make_unique<char[]>(buf_len + 1);
      


    1. 5nw
      07.01.2017 17:31
      +1

      К сожалению, она не всегда знает, сколько ей нужно. На некоторых платформах всегда возвращается -1.


      1. Rend
        07.01.2017 18:56

        Могу лишь предположить, что это старые версии ОС с поддержкой C89/C90, где vsnprintf стандартом вообще не определена, или нестандартные библиотеки. С99 и POSIX (понимаю, с 1003.1-2005) требуют возвращать значение. К сожалению, не нашёл требований к данной функции по стандарту C++; скорее всего, тоже требует.

        Но на возврат -1 действительно, лучше проверять.


    1. kibb
      07.01.2017 23:27
      +1

      Можно пользоваться asprintf()/vasprintf(), которые есть в любой современной libc, и не заботиться о выделении памяти.


    1. Antervis
      09.01.2017 11:37

      но тогда функция по факту отработает дважды


  1. Altren
    07.01.2017 17:12
    +4

    Рекомендую ознакомится с библиотекой fmt: https://github.com/fmtlib/fmt
    По сути она решает все указанные проблемы, имея производительность близкую к printf.


  1. 015z
    07.01.2017 17:12
    +1

    Наследие C

    Строковое форматирование в C осуществляется с помощью семейства функций Xprintf.

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

    Можно вызвать snprintf без указания выходного буфера:
      int res = snprintf(NULL, 0, "I have %d apples and %d oranges, so I have %d fruits", apples, oranges, apples + oranges);
    

    Тогда res будет содержать необходимое количество байт (без нуль-байта). https://linux.die.net/man/3/snprintf:

    Conforming To
    
    The fprintf(), printf(), sprintf(), vprintf(), vfprintf(), and vsprintf() functions conform to C89 and C99. The snprintf() and vsnprintf() functions conform to C99.
    
    Concerning the return value of snprintf(), SUSv2 and C99 contradict each other: when snprintf() is called with size=0 then SUSv2 stipulates an unspecified return value less than 1, while C99 allows str to be NULL in this case, and gives the return value (as always) as the number of characters that would have been written in case the output string has been large enough. 
    

    Да, это два прохода, но знать заранее ничего не нужно, а нужно следить за тем, что остальные аргументы идентичны.

    соответствие количества и типа аргументов и местозаполнителей не проверяется при передаче параметров извне (как в обертке над vsnprintf, реализованной ниже), что может привести к ошибкам при выполнении программы

    Комплятор gcc точно выдает предупреждение, если выставлена опция -Wformat (включена в -Wall). https://linux.die.net/man/1/gcc:
    -Wformat
        Check calls to "printf" and "scanf", etc., to make sure that the arguments supplied have types appropriate to the format string specified, and that the conversions specified in the format string make sense. This includes standard functions, and others specified by format attributes, in the "printf", "scanf", "strftime"...
    

    YouCompleteMe, работающий через libclang, также подсвечивает такие места. Но там есть пара оговорок, посмотрите, пожалуйста.


    1. 5nw
      07.01.2017 17:35

      Да, это два прохода, но знать заранее ничего не нужно, а нужно следить за тем, что остальные аргументы идентичны.
      На некоторых платформах Xprintf() всегда возвращают -1, поэтому к сожалению, двумя вызовами в общем случае не обойтись
      Комплятор gcc точно выдает предупреждение, если выставлена опция -Wformat (включена в -Wall).
      Да, но только при непосредственном вызове. При передаче параметров извне предупреждения не будет:
      printf("%d %d %d\n", 1, 2); // здесь есть warning
      std::cout << format("%d %d %d", 1, 2) << std::endl; // а здесь уже нет
      


      1. 015z
        07.01.2017 19:02

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

        Ткните носом, пожалуйста, на каких именно.


        1. 5nw
          07.01.2017 19:05

          Сейчас точно не помню, кажется на SPARCе со специфическим линуксом
          Там же, насколько я помню, vsnprintf портил arglist
          Деталей по некоторым причинам полностью раскрыть не могу, но платформа очень экзотическая
          Но думаю она не единственная, где такое поведение


          1. 015z
            07.01.2017 19:22

            Я думаю, специфические линуксы и архитектуры потребуют особого внимания и без snprintf, но я понял, спасибо.


            1. 5nw
              07.01.2017 19:37

              Еще судя по ссылке из статьи в винде с Visual Studio и HP-UX тоже всегда возвращается -1, но я не проверял


            1. 5nw
              07.01.2017 19:50

              Сейчас посмотрел mingw в винде, все ок, -1 не возвращается. Значит такое только в студии


              1. Rend
                07.01.2017 20:27
                +1

                Компилятор Visual Studio корректно возвращает требуемую длину при

                snprintf(NULL, 0, format, ...);


    1. 5nw
      07.01.2017 17:55

      За YouCompleteMe спасибо


      1. 015z
        07.01.2017 18:55

        Спасибо Valloric! Отличный инструмент для С-семейства не только для подсветки ошибок, но и для перехода к объявлениям. Конечно, надо повозиться с настройкой и не забывать для каждого проекта делать отдельную конфигурацию.


  1. HolyBlackCat
    07.01.2017 17:12

    del


  1. Vooon
    07.01.2017 17:16
    +1

    Я пользуюсь такой оберткой:


    template<typename ... Args>
    std::string format(const std::string &fmt, Args ... args)
    {
        // C++11 specify that string store elements continously
        std::string ret;
    
        auto sz = std::snprintf(nullptr, 0, fmt.c_str(), args...);
        ret.reserve(sz + 1); ret.resize(sz);    // to be sure there have room for \0
        std::snprintf(&ret.front(), ret.capacity() + 1, fmt.c_str(), args...);
        return ret;
    }

    Она конечно хуже буста (нет проверки типов, хотя это делает компилятор), и не умеет работать со строками, но все же удобна.


    1. 5nw
      07.01.2017 17:19

      del


    1. 5nw
      07.01.2017 17:25

      Если std::snprintf из C++11 работает одинаково на всех платформах, то это неплохой вариант, спасибо.


      1. Vooon
        07.01.2017 17:35

        Я не знаю, этот код работает только на линуксе, и вроде кто-то собирал под макось (но я не знаю результат).
        http://ru.cppreference.com/w/cpp/io/c/fprintf — но на платформе, где printf() не по стандарту нет надежды, что stdc++ будет соответствовать стандарту.


        Еще нужно бы добавить assert на sz >= 0, но если sz = -1, то все равно должно будет упасть на resize().


  1. Alesh
    07.01.2017 17:59
    +1

    Да с форматированием в С++ почему то до сих пор, как в статусе из соц.сетей — "все сложно") Даже странно, не ужели никому реально не надо.


    1. ZaMaZaN4iK
      07.01.2017 19:27

      стандарт меняется довольно медленно. Сторонние библиотеки всегда можно использовать. Благо, что много годных либ header-only.

      Хорошо, что в последнее время за стандарт взялись. Но всё равно, медленно всё проходит…


  1. mikeus
    07.01.2017 18:42

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

    Упомянутое различие для случая с Windows задокументировано в MSDN:
    The snprintf function always stores a terminating null character, truncating the output if necessary. The _snprintf family of functions only appends a terminating null character if the formatted string length is strictly less than count characters.

    Честно говоря я никогда не понимал и не разбирался зачем майкрософт ввел ещё серию функций _snprintf_s. Что в них более безопасного?


  1. Sazonov
    08.01.2017 02:48

    У вас незначительная логическая ошибка в функции from_string. Для неё стоит сделать явную специализацию для типа std::string. Код, который не будет работать:
    auto x = from_string("1 2"); // x == "1";


    1. 5nw
      08.01.2017 02:52

      Я использую ее так:

      auto x = from_string<int>("1 2");
      

      так должно работать


      1. Sazonov
        08.01.2017 20:54
        +1

        Если «так должно работать», то зачем вам шаблоны? Пишите явную имплементацию для типа int. Если же делаете шаблонную функцию, то делайте так, чтобы она работала с любыми типами.

        Проверка (раскомментируйте код, чтобы заработало как надо)

        #include <iostream>
        #include <string>
        #include <sstream>
        
        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 main()
        {
        	auto x = from_string<std::string>("1 2");
        	std::cout << x; // Вывод "1" вместо "1 2"
        	
        	return 0;
        }
        


        1. 5nw
          08.01.2017 21:09

          Если «так должно работать», то зачем вам шаблоны?

          Как раз чтобы не писать миллион имплементаций для всех возможных типов
          Понял, о чем Вы
          Да, это специфика operator<< для строк
          Исправлю, спасибо


  1. Renat060888
    08.01.2017 17:52

    В целом интересно. Но я чето не понял как С++11-шный вариант автора работает с std стрингами. Там же вылезет «error: no matching function for call to 'to_string(std::basic_string&)'»


    1. 5nw
      08.01.2017 17:55

      Странно, но у меня все работает:


      std::string str = "hello";
      std::cout << to_string(str) << std::endl;
      hello

      Вероятно у Вас в коде using namespace std; и вместо шаблонного to_string вызывается std::to_string


  1. Renat060888
    08.01.2017 18:00

    понялс, благодарю) Моя ошибка


  1. newnon
    08.01.2017 19:20

    я уже давно использую эту библиотечку https://github.com/fmtlib/fmt
    и по скорости и по надежности прям то что надо и c#/python стиль и старый добрый C стиль


  1. Shamov
    09.01.2017 12:28

    Любопытно, что при разработке подобных штук люди всегда ограничиваются форматированием самых простейших типов и никогда не пытаются расширить этот тривиальный набор. Например, добавить плейсхолдер для форматирования IP-адреса, для первого (или второго) элемента std::pair, для автоматической подстановки текста ошибки по её коду. Или сделать крутой навороченный плейсхолдер для удобного вывода массивов. Точнее, для общего случая любых контейнеров/ranges. Или же можно было бы вообще выйти за флажки и добавить плейсхолдер для подстановки слова в указанном падеже и числе. Ну, типа, вот так:


    format("счёт пополнен %d %s", 2, ablative("рубль")); // => "счёт пополнен 2 рублями"


  1. dendron
    11.01.2017 00:52
    +1

    Со строками в C++ традиционно всё плохо, к сожалению. Новые костыли оказываются не лучше предыдущих, стандарт распухает, и так снова и снова.