29

Goal: serialize data to JSON.

Issue: i cant know beforehand how many chars long the integer is.

i thought a good way to do this is by using sprintf()

size_t length = sprintf(no_buff, "{data:%d}",12312);
char *buff = malloc(length);
snprintf(buff, length, "{data:%d}",12312);
//buff is passed on ...

Of course i can use a stack variable like char a[256] instead of no_buff.

Question: But is there in C a utility for disposable writes like the unix /dev/null? Smth like this:

#define FORGET_ABOUT_THIS ...
size_t length = sprintf(FORGET_ABOUT_THIS, "{data:%d}",12312);

p.s. i know that i can also get the length of the integer through log but this ways seems nicer.

M.M
  • 138,810
  • 21
  • 208
  • 365
alknows
  • 1,972
  • 3
  • 22
  • 26

7 Answers7

45

Since C is where simple language, there is no such thing as "disposable buffers" -- all memory management are on programmers shoulders (there is GNU C compiler extensions for these but they are not standard).

cant know beforehand how many chars long the integer is.

There is much easier solution for your problem. snprintf knows!

On C99-compatible platforms call snprintf with NULL as first argument:

ssize_t bufsz = snprintf(NULL, 0, "{data:%d}",12312);
char* buf = malloc(bufsz + 1);
snprintf(buf, bufsz + 1, "{data:%d}",12312);

...

free(buf);

In older Visual Studio versions (which have non-C99 compatible CRT), use _scprintf instead of snprintf(NULL, ...) call.

myaut
  • 11,174
  • 2
  • 30
  • 62
  • 1
    Doesn't Visual Studio (at least in recent versions) support a function called `_snprintf` that's similar to the standard `snprintf`? – Keith Thompson Mar 16 '15 at 21:30
  • 1
    @KeithThompson: checked MSDN, it seem that starting with VS2013 they fixed that. Thanks for the note, I've updated my answer – myaut Mar 16 '15 at 21:33
  • 3
    Note: `snprintf()` may also return -1 and has trouble in corner situations as the max value of `size_t` and `int` are rarely the same. There may be an issue with the 2 `snprintf()` should the `locale` change between them and the 2nd `output is longer. – chux - Reinstate Monica Mar 16 '15 at 22:03
  • @chux: of course my code is generic, and should include error checks, including `INT_MAX` and `-1`, and `ssize_t` is not a concern of `snprintf()`. Speaking of changing locale - we will get non-NULL terminated string (so, we should error-check 2nd `snprintf()` too), but at least it won't cause buffer overflow (at least, directly). AFAIK, it is hard to change locale externally. – myaut Mar 16 '15 at 22:17
20

You can call int len = snprintf(NULL, 0, "{data:%d}", 12312) to test how much space you need.

snprintf will print at most size characters, where size is the second argument, and return how many characters would have been necessary to print the whole thing, not counting the terminating '\0'. Because you pass in 0, it won't actually write anything out (and thus will avoid any null pointer exception that would happen by trying to dereference NULL), but it will still return the length that is needed to fit the whole output, which you can use to allocate your buffer.

At that point you can allocate and print to your buffer, remembering to include one more for the trailing '\0':

char *buf = malloc(len + 1);
snprintf(buf, len + 1, "{data:%d}", 12312);
Brian Campbell
  • 322,767
  • 57
  • 360
  • 340
  • Great, thanks. Exactly what i needed. Funny this wasn't in the quick reference. I guess that's what experience amounts for. Regards – alknows Mar 16 '15 at 21:33
  • 1
    You should also mention that if a NULL buffer is used then it will not count the string termination. http://en.cppreference.com/w/c/io/fprintf – alknows Mar 16 '15 at 21:48
  • @Aldi Thanks for pointing that out, updated to include that clarification. – Brian Campbell Mar 16 '15 at 21:55
6

To just obtain the length you can write:

int length = snprintf(NULL, 0, "{data:%d}", 12312);

Note that the return type is int. It may return -1 in case of some sort of error. Make sure your input data doesn't include long strings that might cause the total length to exceed INT_MAX !

M.M
  • 138,810
  • 21
  • 208
  • 365
  • You can easily compute the maximum size (10 assuming 32 bits) + 8 - So 18 – Ed Heal Mar 16 '15 at 21:29
  • 1
    @EdHeal my solution makes no assumption about the sizes of types. I'm sure there is a lot of code out there which broke for just that reason when recompiled for 64-bit target. – M.M Mar 16 '15 at 21:31
  • @mattMcNadd - For speed the solution is just to have a constant - this is one case where the per-processor is handy. – Ed Heal Mar 16 '15 at 21:34
  • my answers focus on correctness over speed :) micro-optimization can come later. YMMV I suppose. – M.M Mar 16 '15 at 21:34
  • I would just give it 100 bytes - More than enough to play with. Memory is cheap – Ed Heal Mar 16 '15 at 21:36
  • @EdHeal one system I program on has 2K of stack – M.M Mar 16 '15 at 21:44
  • That was a luxury programming a BBC B in the old days. But today?! – Ed Heal Mar 16 '15 at 21:45
  • @Ed Heal A buffer of size `CHAR_BIT * sizeof int /3 + 3` is sufficient for all `int` be it 16, 32, 64 or whatever. Although right-sizing buffers is not too difficult, C has a pesky thing call `locale` that can break `char buf[CHAR_BIT * sizeof int /3 + 3]; sprintf(buf, "%d", some_int);`. For performance and safely, I suggest 2x right-sized buffers and this `snprintf()` solution for higher safety. (Which is the only answer to rightly discuss -1 and other `int` concerns). – chux - Reinstate Monica Mar 16 '15 at 21:57
  • @chux - I might be a little Victorian. Over compensation when it is (moreorless) free. Why not add an extra 10 bytes here or there. – Ed Heal Mar 16 '15 at 21:59
  • For the record: `libjson` actually does such optimization: http://code.metager.de/source/xref/avidemux/2.6/avidemux_core/ADM_coreUtils/src/Source/NumberToString.h#18 – myaut Mar 16 '15 at 22:07
  • @Ed Heal Agree a +10 is about reasonable. I sleep easier with 2x. OTOH, `snprintf(..."%f"...)` is the way to go with FP given the potential for 100s (1000s) of unexpected output when the value is supposed to be in a small range but `DBL_MAX` shows up. – chux - Reinstate Monica Mar 16 '15 at 22:09
  • 1
    @chux - Brunel is one of my heroes. Back then they over-engineered stuff. But the bridges etc. are still in use. But the Forth road bridge is getting replaced but the Forth rail bridge is just getting a new coat of paint and the Clifton bridge might get the lines in the middle of the road repainted this year – Ed Heal Mar 16 '15 at 22:13
2

If you check the performance, you will running snprintf without an output buffer will take roughly the same time as a full invocation.

So I recommend you to use a smaller buffer just in case, and only call it a second time if the returned size exceeded the buffer size.

This uses C++'s std::string but I guess you can adapt it for your needs.

std::string format(const char* format, ...) {
    va_list args;
    va_start(args, format);
    char smallBuffer[1024];
    int size = vsnprintf(smallBuffer, sizeof smallBuffer, format, args);
    va_end(args);

    if (size < sizeof smallBuffer) 
        return std::string(smallBuffer);

    char buffer[size  + 1]; /* maybe malloc if it's too big */

    va_start(args, format);
    vsnprintf(buffer, sizeof buffer, format, args);
    va_end(args);
    return std::string(buffer);
}

This code will run 2x faster for strings under 1k compared to the longer ones.

Raúl Salinas-Monteagudo
  • 3,412
  • 1
  • 24
  • 22
2

Calling snprintf(nullptr, 0, ...) does return the size but it has performance penalty, because it will call IO_str_overflow and which is slow.

If you do care about performance, you can pre-allocate a dummy buffer and pass its pointer and size to ::snprintf. it will be several times faster than the nullptr version.

template<typename ...Args>
size_t get_len(const char* format, Args ...args) {
  static char dummy[4096]; // you can change the default size
  return ::snprintf(dummy, 4096, format, args...) + 1; // +1 for \0
}
DAG
  • 417
  • 4
  • 7
0

Printf supports %n format parameter, which means "write position of %n in output string to int value pointed by x-parameter), so:

int x;snprintf(NULL, 0, "Message: %s%n", "Error!",&x);

Should works!

nintyfan
  • 386
  • 3
  • 16
-1

This isn't strictly an answer to your question, but you may find it helpful nonetheless. It is not portable, but if you're running this on glibc, you can simply use asprintf() instead, which will do the memory allocation for you.

Dolda2000
  • 25,216
  • 4
  • 51
  • 92