Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I came here to post exactly that; I think using snprintf() is the proper solution to the "I need to build a string out of many parts in C" problem pretty much every time.

Especially since these functions' return values make it hard to use them to step forward in the buffer, so subsequent calls need to walk over the already-inserted characters again and again. That kind of redundant work makes me grit my teeth, which I guess is a sign that I'm a C programmer at heart. :)

Compare:

    char buf[1024];
    snprintf(buf, sizeof buf, "%s%s", "foo", "bar");
and

    char buf[1024];
    strcpy(buf, "foo");
    strlcat(buf, "bar", sizeof buf);
The latter is given a pointer to the base buffer, so it has to walk past the existing characters in order to find the end where the concatenation should start.

I also find the "size" arguments confusing and weird. I think it's idiomatic in C to (tightly) associate a size with a buffer pointer, and speaking in terms of "there are n bytes available at this location" makes sense to me. So the prototype really should have been

    size_t strlcat_unwind(char *buf, size_t buf_size, const char *src);


Even better, if you don't mind using the heap, there's also asprintf() which figures out the right length and allocates a buffer itself, then returns it to you. Downside is you have to free the pointer returned of course. And you might want to be careful passing it user input without taking a look at the length first. But you have to do that anyway if you use the stack.


Here's something I've found a useful upgrade to asprintf, as it frees the passed-in buffer after expanding the format string. You can just pass the same char ** repeatedly (and also pass it as one of the string arguments corresponding to a %s!) and it'll update the pointer appropriately each time. Makes many kinds of string manipulation very simple.

    int xasprintf(char**p,const char *fmt,..) {
        int n=0;
        char *p2=nullptr;
        if(fmt) {
            va_list v;
            va_start(v);
            n=asprintf(&p2,fmt,v);
            va_end(v);
        }
        if(n>=0) {
            free(*p);
            *p=p2;
        }
        return n;
    }
If you thought asprintf was inefficient, this is worse. But it's very convenient!


By the way, I realized that sprintf can be used to concatenate by using the buffer within the formatting itself, while snprintf prevents it. Is it a safety issue too?

  strcpy(buf, "foo");
  sprintf(buf, "%s%s", buf, "bar"); //buf contains "foobar"

  strcpy(buf, "foo");
  snprintf(buf, sizeof buf, "%s%s", buf, "bar"); //buf contains "bar"


keep in mind that you can't do that, here's a quote that explains it well:

Some programs imprudently rely on code such as the following

    sprintf(buf, "%s some further text", buf);
to append text to buf. However, the standards explicitly note that the results are undefined if source and destination buffers overlap when calling sprintf(), snprintf(), vsprintf(), and vsnprintf(). Depend‐ ing on the version of gcc(1) used, and the compiler options employed, calls such as the above will not produce the expected results.

The glibc implementation of the functions snprintf() and vsnprintf() conforms to the C99 standard, that is, behaves as described above, since glibc 2.1. Until glibc 2.0.6, they would return -1 when the output was truncated.

- printf(3), Linux man-pages 6.04, 2023-04-01


Except snprintf() does not terminate the string if there's no room for the final \0. So technically this should be snprintf(buf, sizeof buf - 1, "%s%s", "foo", "bar"); buf[sizeof buf - 1] = '\0';


I don't think that is correct, my understanding is that it always terminates when possible. FreeBSD's man page [1] has a pretty clear description:

The snprintf() and vsnprintf() functions will write at most size-1 of the characters printed into the output string (the size'th character then gets the terminating `\0'); if the return value is greater than or equal to the size argument, the string was too short and some of the printed characters were discarded. The output is always null-terminated, unless size is 0.

On a more anecdotal front, I did a quick test on Ideone [2] that seems to behave exactly how I would expect it to (and contradicts your description).

In short: always call snprintf() with the exact size of the buffer. No Obi-Wans.

[1]: https://man.freebsd.org/cgi/man.cgi?query=snprintf

[2]: https://ideone.com/CdLfNS


Check the return value! Unless you really want truncated values, but I would assume most people don't.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: