Есть ли способ определить, сколько символов будет записано sprintf?

Я работаю на С++.

Я хочу написать потенциально очень длинную отформатированную строку, используя sprintf (в частности, безопасную версию с подсчетом, например _snprintf_s, но идея та же). Приблизительная длина неизвестна во время компиляции, поэтому мне придется использовать некоторую динамически выделяемую память, а не полагаться на большой статический буфер. Есть ли способ определить, сколько символов потребуется для конкретного вызова sprintf, чтобы я всегда мог быть уверен, что у меня достаточно большой буфер?

Мой запасной вариант: я просто возьму длину строки формата, удвою ее и попробую. Если это сработает, отлично, если нет, я просто удвою размер буфера и попробую еще раз. Повторяйте, пока не подойдет. Не самое умное решение.

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

Может быть, вместо этого сработает fprintf для «/dev/null»/«nul»? Любые другие идеи?

РЕДАКТИРОВАТЬ: В качестве альтернативы, есть ли способ «разбить» sprintf, чтобы он поднимался в середине записи? Если это возможно, он может заполнить буфер, обработать его, а затем начать повторное заполнение с того места, где он остановился.


person Screndib    schedule 05.02.2009    source источник


Ответы (7)


Страница руководства для snprintf говорит:

   Return value
       Upon  successful  return,  these  functions return the number of
       characters printed (not including the trailing '\0' used to  end
       output to strings).  The functions snprintf and vsnprintf do not
       write more than size bytes (including the  trailing  '\0').   If
       the output was truncated due to this limit then the return value
       is the number of characters (not including  the  trailing  '\0')
       which  would  have  been  written  to the final string if enough
       space had been available. Thus, a return value of size  or  more
       means  that  the  output  was  truncated.  (See also below under
       NOTES.)  If an output error is encountered, a negative value  is
       returned.

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

int how_much_space = snprintf(NULL, 0, fmt_string, param0, param1, ...);
person Nathan Fellman    schedule 05.02.2009
comment
По-прежнему лучше сначала попытаться вывести переменную стека фиксированного размера, поскольку подавляющее большинство printf будет меньше определенного размера. Это означает, что подавляющему большинству не нужен print/malloc/print/free, а просто print. Полное поведение требуется только небольшому числу сверх лимита. - person paxdiablo; 05.02.2009
comment
@Pax: возможно, это правда, но это оптимизация производительности. Часто это уместно, но не всегда. Пример того, когда это не так: у вас не так много места в стеке, и вы ожидаете, что подавляющее большинство отпечатков будет больше любого размера буфера, который вы с удовольствием поместите в стек. Таким образом, вы всегда используете кучу. - person Steve Jessop; 06.02.2009
comment
Учитывая этот сценарий, почему бы не выделить заранее выделенную часть памяти для отпечатков в куче и получить выгоду от оптимизации производительности? - person user666412; 24.09.2012
comment
Как sprintf вычисляет фактическое количество необходимых байтов? - person Prashanth; 21.10.2016

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

Для небольшого повышения эффективности вы можете вызвать его с буфером, достаточно большим для нормального случая, и сделать второй вызов snprintf() только в том случае, если вывод усекается. Чтобы убедиться, что в этом случае буферы правильно освобождены, я часто использую объект auto_buffer<>, который обрабатывает динамическую память для меня (и имеет буфер по умолчанию в стеке, чтобы избежать выделения кучи в обычном случае). ).

Если вы используете компилятор Microsoft, у MS есть нестандартный _snprintf(), который имеет серьезные ограничения: не всегда нуль завершает буфер и не указывает, насколько большим должен быть буфер.

Чтобы обойти отказ от поддержки Microsoft, я использую почти общедоступный домен snprintf() от Holger Weiss.

Конечно, если в вашем компиляторе C или C++, отличном от MS, отсутствует snprintf(), код из приведенной выше ссылки должен работать так же хорошо.

person Michael Burr    schedule 05.02.2009

Я бы использовал двухэтапный подход. Как правило, большой процент выходных строк будет ниже определенного порога, и только некоторые из них будут больше.

Этап 1. Используйте статический буфер разумного размера, например 4K. Поскольку snprintf() может ограничивать количество записываемых символов, вы не получите переполнения буфера. Что вы получите от snprintf(), так это количество символов, которое он записал бы, если бы ваш буфер был достаточно большим.

Если ваш вызов snprintf() возвращает меньше 4 КБ, используйте буфер и выйдите. Как уже говорилось, подавляющее большинство вызовов должны делать именно это.

Некоторые не будут, и именно тогда вы перейдете к этапу 2. Если вызов snprintf() не помещается в буфер 4K, вы, по крайней мере, теперь знаете, насколько большой буфер вам нужен.

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

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

Имейте в виду, что не во всех системах есть snprintf() (например, я понимаю, что это _snprintf() в Microsoft C), поэтому вам, возможно, придется найти функцию, которая выполняет ту же работу, или вернуться к решению fprintf /dev/null.

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

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

person paxdiablo    schedule 05.02.2009
comment
К сожалению, функция snprintf() не является стандартной для C++. Я только что попытался использовать его с Visual Studio 2008 Express Edition, и компилятор сообщает, что snprintf не найден. - person jasonmray; 05.02.2009
comment
я думаю, что это _snprintf() в Microsoft C++ - person FryGuy; 05.02.2009
comment
@rubancache, это когда вы используете решение fprintf to /dev/null. - person paxdiablo; 05.02.2009

Как бы то ни было, asprintf — это расширение GNU, которое управляет этой функциональностью. Он принимает указатель в качестве выходного аргумента вместе со строкой формата и переменным числом аргументов и записывает обратно в указатель адрес правильно выделенного буфера, содержащего результат.

Вы можете использовать его так:

#define _GNU_SOURCE
#include <stdio.h>

int main(int argc, char const *argv[])
{
    char *hi = "hello"; // these could be really long
    char *everyone = "world";
    char *message;
    asprintf(&message, "%s %s", hi, everyone);
    puts(message);
    free(message);
    return 0;
}

Надеюсь, это поможет кому-то!

person Alex Reinking    schedule 03.09.2014

Взгляните на CodeProject: CString-clone Using Standard C++. Он использует предложенное вами решение с увеличением размера буфера.

// -------------------------------------------------------------------------
    // FUNCTION:  FormatV
    //      void FormatV(PCSTR szFormat, va_list, argList);
    //
// DESCRIPTION: // This function formats the string with sprintf style format-specs. // It makes a general guess at required buffer size and then tries // successively larger buffers until it finds one big enough or a // threshold (MAX_FMT_TRIES) is exceeded. // // PARAMETERS: // szFormat - a PCSTR holding the format of the output // argList - a Microsoft specific va_list for variable argument lists // // RETURN VALUE: // -------------------------------------------------------------------------

void FormatV(const CT* szFormat, va_list argList)
{
#ifdef SS_ANSI

    int nLen    = sslen(szFormat) + STD_BUF_SIZE;
    ssvsprintf(GetBuffer(nLen), nLen-1, szFormat, argList);
    ReleaseBuffer();
#else
    CT* pBuf            = NULL;
    int nChars          = 1;
    int nUsed           = 0;
    size_type nActual   = 0;
    int nTry            = 0;

    do  
    {
        // Grow more than linearly (e.g. 512, 1536, 3072, etc)

        nChars          += ((nTry+1) * FMT_BLOCK_SIZE);
        pBuf            = reinterpret_cast<CT*>(_alloca(sizeof(CT)*nChars));
        nUsed           = ssnprintf(pBuf, nChars-1, szFormat, argList);

        // Ensure proper NULL termination.
        nActual         = nUsed == -1 ? nChars-1 : SSMIN(nUsed, nChars-1);
        pBuf[nActual+1]= '\0';


    } while ( nUsed < 0 && nTry++ < MAX_FMT_TRIES );

    // assign whatever we managed to format

    this->assign(pBuf, nActual);
#endif
}

person Robert Vuković    schedule 05.02.2009

Я искал ту же функциональность, о которой вы говорите, но, насколько я знаю, что-то настолько простое, как метод C99, недоступно в C++, потому что C++ в настоящее время не включает функции, добавленные в C99 (например, snprintf) .

Лучше всего, вероятно, использовать объект stringstream. Это немного более громоздко, чем четко написанный вызов sprintf, но он будет работать.

person jasonmray    schedule 05.02.2009
comment
Я этого не делал, но я могу понять, почему это было сделано. Вам не нужен C99, так как существуют версии snprintf() для PD. Или, может быть, потому, что вопрос специально задавался для решения printf(), а не для stringstream. Не знаю, я уже давно отказался от попыток понять тех, кто проезжает мимо. - person paxdiablo; 05.02.2009

Поскольку вы используете C++, нет необходимости использовать какую-либо версию sprintf. Проще всего использовать std::ostringstream.

std::ostringstream oss;
oss << a << " " << b << std::endl;

oss.str() возвращает std::string с содержимым того, что вы написали в oss. Используйте oss.str().c_str(), чтобы получить const char *. В долгосрочной перспективе с этим будет намного проще справляться, и он устранит утечки памяти или переполнение буфера. Как правило, если вас беспокоят такие проблемы с памятью, как в C++, вы не используете весь потенциал языка, и вам следует переосмыслить свой дизайн.

person Rob K    schedule 05.02.2009
comment
Предупреждение: в C++ встроено много мелких дополнений в функции потоковой передачи, и они могут сильно вас укусить. В частности, потоки поддерживают локали, которые могут изменить способ форматирования ваших чисел. Числа, которые были выведены в потоке, настроенном на одну локаль, не могут быть прочитаны в потоке, использующем другую локаль. Если вы можете гарантировать, что никакие другие локали никогда не будут использоваться, то все в порядке. Нас это укусило, потому что мы использовали DLL, которая подключалась к хост-приложению, использующему локали. - person AHelps; 30.08.2014