One common use of variable-length argument lists is to implement functions
that are a thin wrapper around the printf()
family of functions.
One such example is a set of error reporting functions.
errmsg.h
#ifndef ERRMSG_H_INCLUDED
#define ERRMSG_H_INCLUDED
#include <stdarg.h>
#include <stdnoreturn.h> // C11
void verrmsg(int errnum, const char *fmt, va_list ap);
noreturn void errmsg(int exitcode, int errnum, const char *fmt, ...);
void warnmsg(int errnum, const char *fmt, ...);
#endif
This is a bare-bones example; such packages can be much elaborate.
Normally, programmers will use either errmsg()
or warnmsg()
, which themselves use verrmsg()
internally.
If someone comes up with a need to do more, though, then the exposed verrmsg()
function will be useful.
You could avoid exposing it until you have a need for it (YAGNI — you aren't gonna need it), but the need will arise eventually (you are gonna need it — YAGNI).
errmsg.c
This code only needs to forward the variadic arguments to the vfprintf()
function for outputting to standard error.
It also reports the system error message corresponding to the system error number (errno
) passed to the functions.
#include "errmsg.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void
verrmsg(int errnum, const char *fmt, va_list ap)
{
if (fmt)
vfprintf(stderr, fmt, ap);
if (errnum != 0)
fprintf(stderr, ": %s", strerror(errnum));
putc('\n', stderr);
}
void
errmsg(int exitcode, int errnum, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
verrmsg(errnum, fmt, ap);
va_end(ap);
exit(exitcode);
}
void
warnmsg(int errnum, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
verrmsg(errnum, fmt, ap);
va_end(ap);
}
Using errmsg.h
Now you can use those functions as follows:
#include "errmsg.h"
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv)
{
char buffer[BUFSIZ];
int fd;
if (argc != 2)
{
fprintf(stderr, "Usage: %s filename\n", argv[0]);
exit(EXIT_FAILURE);
}
const char *filename = argv[1];
if ((fd = open(filename, O_RDONLY)) == -1)
errmsg(EXIT_FAILURE, errno, "cannot open %s", filename);
if (read(fd, buffer, sizeof(buffer)) != sizeof(buffer))
errmsg(EXIT_FAILURE, errno, "cannot read %zu bytes from %s", sizeof(buffer), filename);
if (close(fd) == -1)
warnmsg(errno, "cannot close %s", filename);
/* continue the program */
return 0;
}
If either the
open()
or
read()
system calls fails, the error is written to standard error and the
program exits with exit code 1.
If the
close()
system call fails, the error is merely printed as a warning message, and
the program continues.
Checking the correct use of printf()
formats
If you are using GCC (the GNU C Compiler, which is part of the GNU
Compiler Collection), or using Clang, then you can have the compiler
check that the arguments you pass to the error message functions match
what printf()
expects.
Since not all compilers support the extension, it needs to be compiled
conditionally, which is a little bit fiddly.
However, the protection it gives is worth the effort.
First, we need to know how to detect that the compiler is GCC or Clang
emulating GCC.
The answer is that GCC defines __GNUC__
to indicate that.
See common function
attributes
for information about the attributes — specifically the format
attribute.
Rewritten errmsg.h
#ifndef ERRMSG_H_INCLUDED
#define ERRMSG_H_INCLUDED
#include <stdarg.h>
#include <stdnoreturn.h> // C11
#if !defined(PRINTFLIKE)
#if defined(__GNUC__)
#define PRINTFLIKE(n,m) __attribute__((format(printf,n,m)))
#else
#define PRINTFLIKE(n,m) /* If only */
#endif /* __GNUC__ */
#endif /* PRINTFLIKE */
void verrmsg(int errnum, const char *fmt, va_list ap);
void noreturn errmsg(int exitcode, int errnum, const char *fmt, ...)
PRINTFLIKE(3, 4);
void warnmsg(int errnum, const char *fmt, ...)
PRINTFLIKE(2, 3);
#endif
Now, if you make a mistake like:
errmsg(EXIT_FAILURE, errno, "Failed to open file '%d' for reading", filename);
(where the %d
should be %s
), then the compiler will complain:
$ gcc -O3 -g -std=c11 -Wall -Wextra -Werror -Wmissing-prototypes -Wstrict-prototypes \
> -Wold-style-definition -c erruse.c
erruse.c: In function ‘main’:
erruse.c:20:64: error: format ‘%d’ expects argument of type ‘int’, but argument 4 has type ‘const char *’ [-Werror=format=]
errmsg(EXIT_FAILURE, errno, "Failed to open file '%d' for reading", filename);
~^
%s
cc1: all warnings being treated as errors
$