3
votes

I'm trying to learn about the snprintf and found this answer with the example:

char buf[20] = "";
char *cur = buf, * const end = buf + sizeof buf;
cur += snprintf(cur, end-cur, "%s", "foo");
printf("%s\n", buf);
if (cur < end) {
    cur += snprintf(cur, end-cur, "%s", " bar");
}
printf("%s\n", buf);
free(str);

The thing that is not clear to me is that we allocate a fixed hardcoded buffer size which seems suffer from buffer overflow. In the N1570 I found that (7.21.6.5)

1

#include <stdio.h>
int snprintf(char * restrict s, size_t n,
const char * restrict format, ...);

2 The snprintf function is equivalent to fprintf, except that the output is written into an array (specified by argument s) rather than to a stream. If n is zero, nothing is written, and s may be a null pointer.

So to me it appears as the idiomatic usage would be as follows:

int need_space = snprintf(NULL, 0, "abs %s", "fgh") + 1; //How much to allocate?
char *const str = malloc(need_space * sizeof(char)); //allocate
int written = snprintf(str, need_space, "abs %s", "fgh"); //do format
printf("Need space = %d, written = %d\n", need_space, written);
printf("%s\n", str);

Or this is not common and has another problem?

1
It's got its second argument for a reason. This is an idiomatic C-way to try avoid overflows.bipll
There is no "silent" truncation, see snprintf.3p - Linux manual page paying careful attention to the "RETURN" portion. You validate the return against the buffer size.David C. Rankin
by definition sizeof(char) is 1bruno
One reason that what you show is not necessarily what people do is that there is a perceived cost to formatting the data twice, once to find out how long it will be and a second time to actually do the formatting. In your example with short fixed-length strings, the cost is not great. With more complex formats, more arguments, and especially longer string arguments, there is some cause for concern (but measurement would still be a good idea — how slow is it really?). Some systems (Linux, GNU C Library) have asprintf() and vasprintf(); using these can be a good idea.Jonathan Leffler
Agree on previous comment and just small addition - your approach requires to specify the same formatting string twice, which in case of complex string is error-prone because declaring format strings with preprocessor defines is also a bad practice.grapes

1 Answers

3
votes

The 2x call to snprintf() is a common idiom.

... has another problem?

Problems lie in the corners

Format maintenance

The below suffers from a repeated format - prone to breaking as code ages and and only one line is changed.

// Oops!
int need_space = snprintf(NULL, 0, "abs %s", "fgh") + 1;
char *const str = malloc(need_space * sizeof(char));
int written = snprintf(str, need_space, "abs %s ", "fgh");

Did you notice the difference?

Better to state the format once.

#define FMT_ABS_S "abs %s"
int need_space = snprintf(NULL, 0, FMT_ABS_S, "fgh");
char *const str = malloc(sizeof *str * (need_space + 1u));
int written = snprintf(str, need_space, FMT_ABS_S, "fgh");

Volatility

Code needs to insure the values do not change (non-volatile) between the 2 calls and special concern arise in multi-threaded applications.

Error checking

Code lacked checks. Yet even with checks, how to even handle unexpected results - perhaps just bail?

static const char *fmt_s = "abs %s";
int needed_space = snprintf(NULL, 0, fmt_s, "fgh");
if (needed_space < 0) {
  Handle_EncodingError();
}
char * const str = malloc(sizeof *str * (needed_space + 1u));
if (str == NULL) {
  Handle_OutOfMemory();
}
int written = snprintf(str, needed_space, fmt_s, "fgh");
if (written < 0 || written > needed_space) {
  Handle_Error();
}

Going twice though

2 calls can be wasteful in select situations, yet commonly the impact is less than thought. @Jonathan Leffler

Yet for me, the "what to do if allocation fails" is a show stopper anyways and code reports the error and maybe exit.

Selectively, once is enough

When the s*printf() format is tightly controlled, I see 1 call as sufficient.

// int to string

#define LOG2_N 28
#define LOG2_D 93
// Number of char needed for a string of INT_MIN is log10(bit width) + 3
#define INT_SIZE ((sizeof(int)*CHAR_BIT-1)*LOG2_N/LOG2_D + 3)

char s[INT_SIZE * 2];  // I like to use 2x to handle locale issues, no need to be stingy here.

int len = snprintf(s, sizeof s, "%d", i);

if (len < 0 || (unsigned) len >= sizeof s) {
  Handle_VeryUnusualFailure();
}