1

case 1:

printf("%*d", 10, 5)

output :

_________5

(I am using _ to denote blank spaces )


case 2:

printf("%*d", 10.4, 5)

expected output:

______0005

But goes for infinite loop


why is this behavior being showed by %*d for decimal "field width precision prefix" ?

chqrlie
  • 131,814
  • 10
  • 121
  • 189
  • 6
    You told `printf` you were going to give it an int but you gave it a double instead. – Kevin Aug 19 '21 at 14:14
  • It is not an infinite loop, it is just that nobody has the time to wait long enough. A double interpreted as an integer will (generally) be a very large number. So you get a gigantic number of spaces, they take a very long time to print. – Hans Passant Aug 19 '21 at 14:16
  • @Kevin iam telling the `printf` iam giving a integer number by using `d` format specifier what and i have given `5` as the number to be printed , any offenses ? – S Mahesh Kumar Aug 19 '21 at 14:17
  • @HansPassant iam not getting how iam specifying the number to be given as integer ? does `"%*d"` specifies only integer number to be placed before format specifier `d` ? – S Mahesh Kumar Aug 19 '21 at 14:19
  • 2
    @SMaheshKumar `*` always expects an integer for the width; even `%*f` would expect an integer -- followed by double. – Yakov Galka Aug 19 '21 at 14:22
  • @YakovGalka Thanks for your reply unable to realize this small thing, bought me a bit of confusion – S Mahesh Kumar Aug 19 '21 at 14:24
  • 1
    C, or any particular C implementation has a scheme to represent `int` values. It stores them using a binary numeral system with a fixed number of bits. It has a different scheme for representing `double` values, and `10.4` is a `double` constant. It does not use a plain binary system; it uses a scheme where there is an exponent field with a built-in offset plus some other bits for the significand. The constant `10.4` is represented with the bits 0100000000100100110011001100110011001100110011001100110011001101. So you passed bits `printf` was not expecting. – Eric Postpischil Aug 19 '21 at 14:24
  • Likely `int` is 32 bits in your C implementation and `double` is 64. So you passed too many bits. And they may have been in the wrong place. It is common that `int` arguments are passed in general processor registers and `double` arguments are passed in special floating-point registers. Either way, `printf` did not find bits it could correctly interpret as an `int`, so it did not find the value you tried to pass. – Eric Postpischil Aug 19 '21 at 14:26
  • @EricPostpischil your explanation cleared a lot of things !. thanks ! – S Mahesh Kumar Aug 19 '21 at 14:30

4 Answers4

4

You told printf using the * format to expect a width integer in the argument list. You gave it a floating-point argument, 10.4. C gets confused when it expects an integer and a float is there instead. You likely intended this:

printf("%*.*d", 10, 4, 5);

with width and precision being represented each by its own separate integral argument.

Amadan
  • 191,408
  • 23
  • 240
  • 301
  • 1
    @EricPostpischil You are right, I said it colloquially (as a floating point type), not a C `float` type. I will correct. – Amadan Aug 19 '21 at 14:20
2

The second line, printf("%*d",10.4,5) leads to undefined behavior. The function printf expects an int but is given the double 10.4 instead.
The handling of the variable number of parameters of printf, the format string and the corresponding types is complex, and goes deep into compiler construction. It's difficult to trace what happens exactly and can vary from compiler to compiler.

See here for the exact specifications of the printf format specifiers.

Addition:

Let's see if we can trace what exactly happens in GNU's implementation of the standard library. First, looking at the source for printf. It uses varargs and a function called __vfprintf_internal. The latter is defined here (line 1318). On line 1363, a sanity check is performed using a macro, but it only checks if the format string pointer is not NULL. Line 1443:

int prec = -1;  /* Precision of output; -1 means none specified.  */

Line 1582, specifying the argument as an int in case the * modifier was used:

prec = va_arg (ap, int);

From here on, the precision is processed as an int. Let's look at the implementation of va_arg to see what happens if it is given a double. It is part of the stdarg header of GCC, see here, line 49:

#define va_arg(v,l) __builtin_va_arg(v,l)

And now it gets complex. __builtin_va_arg isn't explicitly defined anyway, but is part of GCC's internal representation of the C language. See the parser file. That means that we cannot read concrete types in the source files anymore. We can obtain some more information on the processing of varargs in the builtins.c file. From now on I can only guess what happens. The processing appears to start in expand_builtin_va_start which takes a tree parameter and returns an rtx (RTL Expression) object. This object is a constant and probably has the double type mode. I'm assuming the compiler processes double expression until it knows what (machine specific) bit values it has to write in the executable. Since, evidently, the floating point number is not truncated to an int, I wouldn't be surprised if the value would actually correspond, and later will be interpreted as, a more or less random value (e.g. 77975616). It may be also conceivable that the memory of the program would misaligned when, e.g., the type of a double (usually 8 bytes) is larger than an int (usually 4 bytes). More on the implementation of varargs here. Whatever sort-of random value the integer could take would be processed back in the process_arg(fspec) back in vfprintf-internal.c.

Additional curiosity:

If printf is given a float as specifier, even if it is explicitly cast, GCC will still give a warning that the value is a double:

warning: field width specifier ‘*’ expects argument of type ‘int’, but argument 2 has type ‘double’ [-Wformat=]
   10 |     printf("%*d\n", (float) 12.3F, 5);
      |             ~^~     ~~~~~~~~~~~~~
      |              |      |
      |              int    double
Yun
  • 3,056
  • 6
  • 9
  • 28
  • 1
    Re “The variable number of parameters of printf is implemented using a macro and therefore no type checking can occur”: The processing of parameters in `printf` can be implemented in various ways; it is not not necessarily implemented with a macro. And it is not a correct inference that the use of macros would mean no type-checking can occur. A C implementation could be designed to perform type-checking of arguments passed by the variable argument mechanism. – Eric Postpischil Aug 19 '21 at 14:28
  • @EricPostpischil Thank you. I was taught that `printf` always used `va_args`, which in turns uses macros. I'll update the answer and look into this to see if I can get a solid reference. If you have any, please let me know. – Yun Aug 19 '21 at 14:44
1

When you pass 10.4 into printf, it’s treated as a double. However, when printf sees the * character, it tries to interpret the argument containing the number of characters to print as an int. Despite the fact that you and I can intuitively see how to treat 10.4 as an integer by rounding down to 10, that’s not how C sees things. This results in what's called undefined behavior. On some platforms, C might treats the bytes of the double 10.4 as an integer, producing an absolutely colossal integer rather than the expected 10. On other platforms, it might read other data expecting to find an int argument, but instead which holds some other unexpected value. In either case, the result is unlikely to be the nice "interpret the value as 10" that you expect it to be.

templatetypedef
  • 362,284
  • 104
  • 897
  • 1,065
1

Use -Wall -Wextra to see more warnings. You will discover:

<source>:30:12: warning: field width specifier '*' expects argument of type 'int', but argument 2 has type 'double' [-Wformat=]
   30 |   printf("%*d", 10.4, 5);
      |           ~^~   ~~~~
      |            |    |
      |            int  double

It is undefined behaviour.

Now an example what is happening in the printf function:

int foo(const char *fmt, ...)
{
    va_list va;
    int retval = 0;

    va_start(va, fmt);
    retval = va_arg(va, int);
    return retval;
}

unsigned bar(const char *fmt, ...)
{
    va_list va;
    unsigned retval = 0;

    va_start(va, fmt);
    retval = va_arg(va, unsigned);
    return retval;
}

int main(void)
{
    printf("as int %d\n", foo("", 10.4));
    printf("as uint %u\n", bar("", 10.4));
}

And let's execute it: https://godbolt.org/z/EsMaM8EPs

enter image description here

0___________
  • 60,014
  • 4
  • 34
  • 74