Search code examples
ccompiler-warningsgcc-warning

Missing GCC "-Wformat" warnings for "%p" with char* etc


If I compile the following code with -std=c17 -Wall -Wextra -Wpedantic with GCC 13.2.0, I get no warnings, despite not using void* in arguments corresponding to "%p" format specifiers.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

int main()
{
    const char cstr[] = "ABC";
    size_t size = sizeof cstr;
    const uint8_t ustr[] = "ABC";
    const int8_t sstr[] = "ABC";
    const char* pcstr = cstr;
    const uint8_t* pustr = ustr;
    const int8_t* psstr = sstr;
    printf("cstr ptr:  %p\n", cstr);
    printf("size ptr:  %p\n", (void*)&size); // we need cast to prevent Wformat
    printf("&cstr ptr: %p\n", (void*)&cstr); // we also need this cast
    printf("pcstr:     %p\n", pcstr);
    printf("ustr ptr:  %p\n", ustr);
    printf("pustr:     %p\n", pustr);
    printf("sstr ptr:  %p\n", sstr);
    printf("psstr:     %p\n", psstr);
    return 0;
}

After reading Q&A *What is and how to solve the warning: format ‘%p’ expects argument of type ‘void *’, but argument 2 has type ‘int ’ [-Wformat=] when printing it out, should I expect undefined behavior here? I tried several searches, but you'll understand how hard these can be for such a specific combination.

Could it be that void* and char-ish* share some properties I am missing or is this just a case for missing warnings in GCC? Hopefully someone here is able to shed some light on this issue.


Solution

  • You're not getting a warning in the cases without the cast because a void * is required to have the same representation as a pointer to a character type, i.e. char, signed char, and unsigned char, or any type which is a typedef of one of these.

    This is dictated by section 6.2.5p28 of the C standard:

    A pointer to void shall have the same representation and alignment requirements as a pointer to a character type. 48)

    1. The same representation and alignment requirements are meant to imply interchangeability as arguments to functions, return values from functions, and members of unions.

    This is in part due to the history of the C language to a time before void * existed and char * was used as a generic pointer type.

    However, section 7.21.6.1p9 regarding the fprintf function states:

    If a conversion specification is invalid, the behavior is undefined. If any argument is not the correct type for the corresponding conversion specification, the behavior is undefined.

    So while this appears to in fact be undefined behavior by the letter of the standard, GCC (by nature of the warnings it gives) seems to allow a char * to be used in places where a void * is expected as an extension due to the requirement of the the two types having the same representation.

    So I would conclude that such constructs are safe in GCC, though not necessarily on other compilers. For example, it's conceivable that some compilers may use a calling convention that passed a void * to a function differently than how it passes a char *, but again that seems to be a stretch given the representation requirements.

    In fact, section 7.16.1.1p2 regarding the va_arg macro states the following:

    ... If there is no actual next argument, or if type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined, except for the following cases:

    • one type is a signed integer type, the other type is the corresponding unsigned integer type, and the value is representable in both types;
    • one type is pointer to void and the other is a pointer to a character type.

    So assuming that fprintf/printf uses va_arg to read variadic arguments, the behavior would be defined by the above, but of course that's making an assumption about the behavior of the printf family of functions.

    In my opinion, this is a defect in the standard that should be addressed to make such conversions well-defined.