After writing swnprintf
and sbprintf
in C (Encapsulating snprintf to avoid repetition of sizeof), I've written a C++ version of them:
swnprintf
:
Similar to snprintf
. Properties: (properties not mentioned shall be the same as snprintf
)
- It writes the actual value of characters written (excluding the NUL terminator) through a pointer, instead of returning it.
- The user can explicitly discard that value by passing
NULL
. - If a NUL character has been written in the middle of the string (eg.:
swnprintf(buf, &w, sizeof(buf), "%c%c%c", 'a', 0, 'b');
), the last NUL written is not counted, but any other characters until that one, including NULs are counted. In this example,w == 3
after the function call. - If the string is truncated, the value written is the actual number of characters written, and not the value that would have been written had
n
been sufficiently large. - If there's an error in the internal call to snprintf, the resulting string is unreliable (
written
is also unreliable) and thereturn
value is negative. If there's truncation, the string is valid and thereturn
value is positive. If there's no error, thereturn
value is 0. errno
is set on any error. Truncation:ENOMEM
. The absolute value of the return value is the same as errno.
Usage:
- Example where the user doesn't care about truncation:
char buf[BUFSIZ];
ptrdiff_t tmp;
ptrdiff_t len;
if (alx::swnprintf(buf, &tmp, sizeof(buf), "text, num=%i", 7) < 0)
goto err;
len = tmp;
if (alx::swnprintf(&buf[len], &tmp, sizeof(buf) - len, "2nd part") < 0)
goto err;
len += tmp;
- Example where the user cares about truncation and doesn't about len:
char cmd[_POSIX_ARG_MAX];
ptrdiff_t tmp;
if (alx::swnprintf(cmd, &tmp, sizeof(cmd), "%s ", "cat"))
goto err;
if (alx::swnprintf(&cmd[tmp], NULL, sizeof(cmd) - tmp, " %s ", "main.c"))
goto err;
system(cmd);
sbprintf
:
This is a higher abstraction than swnprintf
. It is designed to only accept arrays as input. It is safer because it calculates internally the size of the buffer, so the user has less chance of writing buggy code. The down side is that it is less flexible (it can only write at the beginning of a buffer). Apart from that, the behaviour is the same as in swnprintf
.
Properties: (properties not mentioned shall be the same as swnprintf
)
- It shall only compile if the string is a
char []
and not achar *
. - It is impossible to write past the buffer, because the user doesn't input its size.
Usage:
- Example where the user doesn't care about truncation:
char buf[BUFSIZ];
ptrdiff_t len;
if (alx_sbprintf(buf, &len, "text, num=%i", 7) < 0)
goto err;
- Example where the user cares about truncation and doesn't about len:
char cmd[_POSIX_ARG_MAX];
if (alx_sbprintf(cmd, NULL, "%s %s", "cat", "main.c"))
goto err;
system(cmd);
Implementation:
swnprintf
:
swnprintf.hpp
:
#ifndef ALX_STDIO_PRINTF_SWNPRINTF_HPP
#define ALX_STDIO_PRINTF_SWNPRINTF_HPP
#include <cstdarg>
#include <cstddef>
namespace alx {
int swnprintf(char *__restrict__ str, ptrdiff_t *__restrict__ written,
ptrdiff_t nmemb, const char *__restrict__ format, ...);
} /* namespace alx */
#endif /* libalx/base/stdio/printf/swnprintf.hpp */
swnprintf.cpp
:
#include "libalx/base/stdio/printf/swnprintf.hpp"
#include <cerrno>
#include <cstdarg>
#include <cstddef>
#include <cstdio>
namespace alx {
int swnprintf(char *__restrict__ str, ptrdiff_t *__restrict__ written,
ptrdiff_t nmemb, const char *__restrict__ format, ...)
{
va_list ap;
int len;
if (nmemb < 0)
goto neg;
va_start(ap, format);
len = vsnprintf(str, nmemb, format, ap);
va_end(ap);
if (written != NULL)
*written = len;
if (len < 0)
goto err;
if ((unsigned)len >= nmemb)
goto trunc;
return 0;
err:
return -errno;
trunc:
if (written)
*written = nmemb - 1;
errno = ENOMEM;
return ENOMEM;
neg:
errno = EOVERFLOW;
return -EOVERFLOW;
}
} /* namespace alx */
sbprintf
:
sbprintf.hpp
:
#ifndef ALX_STDIO_PRINTF_SBPRINTF_HPP
#define ALX_STDIO_PRINTF_SBPRINTF_HPP
#include "libalx/base/assert/assert.hpp"
#include "libalx/base/stdio/printf/swnprintf.hpp"
namespace alx {
/*
* int alx_sbprintf(char buff[__restrict__], ptrdiff_t *__restrict__ written,
* const char *__restrict__ fmt, ...);
*/
#define alx_sbprintf(buff, written, fmt, ...) ( \
{ \
\
alx_static_assert_char_array(buff); \
alx::swnprintf(buff, written, sizeof(buff), fmt, ##__VA_ARGS__);\
} \
)
} /* namespace alx */
#endif /* libalx/base/stdio/printf/sbprintf.hpp */
Macros used by the code above:
assert.hpp
:
#ifndef ALX_ASSERT_ASSERT_HPP
#define ALX_ASSERT_ASSERT_HPP
#include <cassert>
#include <type_traits>
namespace alx {
#define alx_static_assert_array(a) do \
{ \
\
static_assert(std::is_array <typeof(a)>::value, "Not a `[]`!"); \
} while (0)
#define alx_static_assert_char_array(a) do \
{ \
\
alx_static_assert_array(a); \
static_assert(std::is_same <char, typeof((a)[0])>::value, \
"Not a `char[]`!"); \
} while (0)
} /* namespace alx */
#endif /* libalx/base/assert/assert.hpp */
Is there anything that can be improved in these two functions? I'm starting with C++ (I'm used to C), so I don't know if there are more efficient/safe methods to do this.
Improvements in the usage (interface) are very welcome.
Is ENOMEM
adecuate when the string has been truncated? My other candidate was EOVERFLOW
, but it was already being used by snprintf
so I thought it might be better to choose a different one.
Edit
The reasons behind so many goto
s are:
Unconditional statements are easier to understand and follow: The error-free path has the minimum conditional statements possible, and they are as short as possible, and therefore, the main code is more compact vertically. When you only want to know what the function does, you can read the first part of the body, and assume the
goto
s will do some cleanup job. Given that eye movement is easier horizontally than vertically, that makes the code easier for the eyes.Nesting is reduced: Those
if
s after thegoto
would be nested to theif
that reports the error. With 8-char tabs, that lets you have more characters per line. Also less eye movement (in this case horizontal).Use of braces is reduced: That's a lot of almost-empty lines saved, and therefore, again less vertical movement for the eyes.
Errors by not updating individual exit points when making modifications are prevented: I said that when you want to know what the function does, you read the first part; when you want to know how the function handles error conditions, you read the last part. All error handling is together, so again, less vertical movement. When reading this part you can know where the
goto
s come from by their labels (I hope they are meaningful enough), and focus only on the error handling itself.Although it can help the optimizer to remove duplicated code some times, that's a secondary reason for me; and sometimes I've seen it the other way (less efficient code). When the function is not critical, I just don't care; when it is, I do whatever is best.
goto
. It was show in the60's
that nearly all uses ofgoto
can be replaced with a better higher level structure. It was then shown in the70's
that goto produces hard to read and hard to maintain spaghetti code. Even in C the usage ofgoto
is rare (there was a thing about single exit point for a while) but in C++ we don't mind multiple exit points as RAII cleans up things so usage ofgoto
is practically non existent. In my 40 years of development I have used it twice in production both (in hindsight) were wrong. \$\endgroup\$goto
in my code, and it is more or less the same as in the kernel. Basically I move error handling with agoto
so that it is after the mainreturn
of the function. I avoid deep nesting with that. However, if there is a better C++ way, I'd like to know; I have to say I don't know much C++ yet \$\endgroup\$CHES KOBLENTS
are well though out and documented. If you have a reason then you should document your reasons, then we would be able to tell if your usage is justified. \$\endgroup\$goto
\$\endgroup\$