Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unexpected compiler warning - printf format specifiers

I have the following warning generated by a printf():

warning: format '%llu' expects argument of type 'long long unsigned int', but argument 7 has type 'uint64_t' {aka 'long unsigned int'} [-Wformat=]

Although I want to create a warning-free code, I think that the compiler is way too over-zealous. Because:

  • long unsigned is unsigned and has 64 bits (it is there, in the text of the message)
  • long long unsigned is also unsigned, and cannot be shorter than 64 bits (C standard, in comparison to unsigned long), and also cannot have more than 64 bits (hardware limitation, not enough bits in the processor registers).

So for any practical purpose, long unsigned and long long unsigned are actually the same thing, right?

Even more, while long unsigned can have 32bits on some platforms (e.g., Widows), long long unsigned is highly unlikely to have another number of bits (than 64). long long was created especially to support 64 bits.

So what is going on? How should I proceed? What should I understand?


Side question:

Let's assume that the following is true:

typedef long unsigned int uint64_t;

And let's assume that on some platform, long unsigned becomes 32 bits, and in that case, long long unsigned is not anymore the same as long unsigned. But then uint64_t is not 64 bits anymore - which means that the definition of uint64_t will have to change to remain 64 bits. So in the end, long long unsigned and uint64_t will still be the same size and sign-ness.

So is my thinking correct, that the compiler should not have given me that message at all? Because it is not applicable, no matter how things are, or will be?


I currently use Codeblocks with Cygwin C compiler under Windows - in order to have long unsigned of 64 bits, instead of 32.

The command line for compiling (copy-paste):

gcc.exe -Wshadow -Winit-self -Wredundant-decls -Wcast-align -Wundef -Wfloat-equal -Winline -Wunreachable-code -Wmissing-declarations -Wswitch-enum -Wswitch-default -Wmain -pedantic -Wextra -Wall -m64 -O0 -c Z:/_progs/_progsData/cb/myprog.c -o obj/Debug/myprog.o

like image 337
virolino Avatar asked Sep 15 '25 12:09

virolino


2 Answers

The warning underscores a portability issue. Using %llu for a uint64_t would have undefined behavior on architectures where uint64_t is an unsigned long and unsigned long long would have 128 bits, which would make sense if 128-bit arithmetics are supported.

This would not happen on legacy systems where long is still 32 bits, but the C Standard does support this possibility.

You can fix the code in different ways:

  • use the PRIu64 macro defined in <inttypes.h> (quite ugly and convoluted, but this is the correct solution):

    int print_uint64(uint64_t val) {
        return printf("%" PRIu64, val);  // a bit cryptic but correct
    }
    
  • cast the values as (unsigned long long) (ugly and verbose too):

    int print_uint64(uint64_t val) {
        return printf("%llu", (unsigned long long)val);
    }
    
  • use %lu... but this would make your code non portable to legacy architectures where long still has just 32 bits.

  • use your own type for 64-bit ints typedef unsigned long long u64; and you can use %llu on all platforms where long long has 64 bits, and worry about portability to 128 bit systems later:

    #include <limits.h>
    #include <stdint.h>
    #include <stdio.h>
    
    #if ULLONG_MAX == UINT64_MAX
    typedef unsigned long long u64;
    typedef          long long i64;
    #else
    #error "this platform does not have 64-bit long long types"
    #endif
    
    int print_u64(u64 val) {
        return printf("%llu", val);
    }
    
like image 100
chqrlie Avatar answered Sep 17 '25 02:09

chqrlie


  • long unsigned is unsigned and has 64 bits (it is there, in the text of the message)

It may have 64 bits on your system but is not specified to have that.

So for any practical purpose, long unsigned and long long unsigned are actually the same thing, right?

No, they are distinct types. If they were the same, the below would compile, but it doesn't, no matter if the word length of unsigned long is the same as that of unsigned long long:

void foo(unsigned long long) {}

int main() {
    unsigned long ul = 123;
    _Generic(ul, unsigned long long: foo)(ul);
}

So you are using an invalid conversion specification for the type of variable you've provided.

From C23 (first post publication, n3220):

7.23.6.1 The fprintf function
  1. If a conversion specification is invalid, the behavior is undefined. fprintf shall behave as if it uses va_arg with a type argument naming the type resulting from applying the default argument promotions to the type corresponding to the conversion specification and then converting the result of the va_arg expansion to the type corresponding to the conversion specification. Note 329: )The behavior is undefined when the types differ as specified for va_arg.
3.5.3.1 undefined behavior

behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this document imposes no requirements

How should I proceed?

Make sure you always use the correct conversion specification.

For uint64_t, the correct conversion specification is available through the macro PRIu64 from <inttypes.h>.

uint64_t var = 123;
printf("The value is %" PRIu64 " and here the string continues\n", var);

What should I understand?

You need to understand that even if unsigned long and unsigned long long have exactly the same number of bits, they are not the same thing and since using an invalid conversion specification makes the program have undefined behavior, the compiler is doing a good thing by emitting a warning,

like image 22
Ted Lyngmo Avatar answered Sep 17 '25 01:09

Ted Lyngmo