3

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.

5
  • "Undefined behaviour" means the standard defines no requirement. Normally the compiler does nothing specific to ensure consistent behaviour (which is implementation defined behaviour). As such where the pointer types are of the same size and byte order, you would expect no difference. The real question is why it does not issue a warning - that is implementation defined. Commented Apr 18, 2024 at 12:24
  • 2
    This looks like one of those "technically UB but always works in practice" warnings. It seems that Clang warns about this and GCC doesn't. Commented Apr 18, 2024 at 12:25
  • @Clifford Isn't byte order irrelevant for void*? Commented Apr 18, 2024 at 12:36
  • 1
    @Wolf purely theoretically if different pointer types may differ in size, it is also possible they could differ in byte order. The point being for %p to interpret a pointer bit pattern correctly it has to be identical to that of the void* it assumes was passed. Not just byte order perhaps; 8086 segment:offset addressing raises some interesting issues we thankfully need not worry about for the most part. Commented Apr 18, 2024 at 12:44
  • Also, the C standard requires that every kind of pointer to object should be convertible to void* and back. Which in practice means that every pointer to object ought to use a compatible format and the same one will be used by void* too. Or how else are you going to sate this C standard requirement in any sensible way? Commented Apr 18, 2024 at 14:15

1 Answer 1

10

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.

Sign up to request clarification or add additional context in comments.

6 Comments

That's the kind of answer I was hoping for. But I wasn't able to find it myself. Thanks a lot.
“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 * -- you mean like floating arguments?
The last sentence of 7.6.11.1p2 is also relevant here: "... if [the type argument to va_arg] is not compatible with the actual type of the next argument ... the behavior is undefined ... except if ... [one of the two] is a pointer to void and the other is a pointer to a character type." This guarantees that void * and char * are interchangeable when passed to a variadic function that uses va_arg to access the variadic arguments. However, I can't find any requirement for variadic standard library functions to (behave as-if they) use va_arg to access variadic arguments.
(I believe the usual "specific beats general" interpretation rule would mean that 7.21.6.1p9 has the last word, but I also believe the committee's intent here was to allow char * to be used with %p without an explicit cast.)
If an arg is passed in a reg (either FP or GPR), it must be spilled to the stack frame when doing &arg. This is also true with a function scoped variable that is optimized to reside in a register. Consider: void bar(int *y) { *y += 37; } ; int foo(int x) { bar(&x); x += 192; return x; }
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.