4

Visual Studio 2015 introduced two new warnings, C4473 and C4477, which inform when a string formatting function has a mismatch between the format string and the associated variadic arguments:

warning C4473: 'printf' : not enough arguments passed for format string
warning C4477: 'printf' : format string '%p' requires an argument of type 'void *', but variadic argument 1 has type 'int'

Those warnings are very helpful and have been supported for a while by other popular compilers (gcc and clang, with the -wformat option I believe, although I'm much less familiar with these compilers).

Now my problem is that I want to use a custom Log(format, ...) function to handle logging, that would do additional work (for example, write to a file and the console, or add a timestamp).

But for the sake of this question, let's just assume I'm simply wrapping a call to printf:

void Log(const char * format, ...)
{
    va_list args;
    va_start(args, format);
    printf(format, args);
    va_end(args);
}

By doing that, I don't have the warnings displayed above if I call my Log function with mismatching arguments:

printf("Error: %p\n", 'a'); // warning C4477
printf("Error: %p\n");      // warning C4473
Log("Error: %p\n", 'a');    // no warning
Log("Error: %p\n");         // no warning

Is there a way to tell the compiler that it should check the variadic arguments of my function the same way it does with printf? Especially for MSVC compiler but a solution that works for gcc and clang would be appreciated too.

Benlitz
  • 1,952
  • 1
  • 17
  • 30
  • For gcc and probably clang, there's the [format attribute](https://gcc.gnu.org/onlinedocs/gcc-8.2.0/gcc/Common-Function-Attributes.html#Common-Function-Attributes). – Shawn Jan 04 '19 at 03:02
  • 1
    Visual C++ have the corresponding [`__declspec`](https://learn.microsoft.com/en-us/cpp/cpp/declspec?view=vs-2017) keyword, but it doesn't seem to have specifiers for format strings. You could find the declaration of e.g. `printf` in the header files, and see if there's some special declaration keywords or identifiers for it. And if not, then you're out of luck. – Some programmer dude Jan 04 '19 at 03:09
  • Perhaps in debug mode have calls to `Log()` also call `snprintf(0,0 ...;`? ( a bit sloppy though for args with side effects.) – chux - Reinstate Monica Jan 04 '19 at 04:18

2 Answers2

5

I don't know what's available in VS 2015 or VS 2017 (a semi-casual search at Microsoft documentation didn't provide any illumination). However, GCC and Clang both support a declarative function attribute:

__attribute__((format(printf(,n,m)))

which can be factored into reasonably portable code like this:

#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 */

…

extern NORETURN void err_abort(const char *format, ...) PRINTFLIKE(1,2);
extern NORETURN void err_error(const char *format, ...) PRINTFLIKE(1,2);

…

extern void err_logmsg(FILE *fp, int flags, int estat, const char *format, ...) PRINTFLIKE(4,5);
…
extern void err_remark(const char *format, ...) PRINTFLIKE(1,2);

The PRINTFLIKE(n,m) macro says that the printf() format string is argument n, and the actual arguments start at m. Most of these are like printf() with the format string as the first argument and the data following. The err_logmsg() function has more control options before the format string at argument 4, but the format arguments start at 5, immediately afterwards, somewhat like fprintf() has its format string as argument 2 and the arguments start at argument 3.

It would be feasible to devise a function with arguments between the format string and the variable argument list, such as:

extern NORETURN void err_pos_error(const char *format, const char *filename, int lineno, const char *function, ...) PRINTFLIKE(1,5);

which might be invoked like this:

err_pos_error("Failed to open file '%s': %d - %s\n", __FILE__, __LINE__, __func__, filename, errno, strerror(errno));

We can debate whether that's a good design (it probably would be better to put the __FILE__, __LINE__, and __func__ arguments before the format string, not after, for a variety of reasons), but it is a feasible design that demonstrates non-consecutive numbers in the PRINTFLIKE macro — or use of __attribute__((format(printf,n,m))).

The NORETURN stuff is macro support for identifying functions that do not return:

#if !defined(NORETURN)
#if __STDC_VERSION__ >= 201112L
#define NORETURN      _Noreturn
#elif defined(__GNUC__)
#define NORETURN      __attribute__((noreturn))
#else
#define NORETURN      /* If only */
#endif /* __STDC_VERSION__ || __GNUC__ */
#endif /* NORETURN */

The code on which I'm basing this is available in my SOQ (Stack Overflow Questions) repository on GitHub as files stderr.c and stderr.h in the src/libsoq sub-directory.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
1

So it seems that I'm indeed out of luck with Visual Studio.

As mentioned by Jonathan in his answer, it is possible to do this with both GCC and Clang. This is also explained in this answer.

However, while Visual Studio seems to output warnings for printf and a bunch of other standard functions, this is more or less hard-coded in the compiler and is not extensible to custom functions.

There is an alternative tho, which I decided not to use (I will explain why). Microsoft provides what they call SAL annotation (for source code annotation language). It is possible to annotate a function with things like _Printf_format_string_ in order to get what I was asking. This is described in this answer for example.

The drawback is, this is by default totally ignored by the compiler. These annotations are actually evaluated only if you enable the Code Analysis, either with the /analysis parameter or from the property window of your project. This analysis does a lot of checks; by default it uses the Microsoft Native Recommended Rules, but it's possible to customize what is to be checked, and even go down to only checking for string formatting.

But even to that point, the overhead in compilation time, for a relatively small project like mine, makes it not worth the pain.

Benlitz
  • 1,952
  • 1
  • 17
  • 30