4

Is this code undefined behavior?

extern long f(long x);

long g(int x)
{
    return f(x);
}

According to the C11 standard, in 6.5.2.2 §6:

If the function is defined with a type that includes a prototype, and [...] the types of the arguments after promotion are not compatible with the types of the parameters, the behavior is undefined.

In the example above, the function f is defined with a type that includes a prototype and the type of the argument x is int while the type of the parameter x is long. According to 6.2.7 §1:

Two types have compatible type if their types are the same.

Therefore, long and int are not compatible, so the behavior is undefined, right?

However, in 6.5.2.2 §7:

If the expression that denotes the called function has a type that does include a prototype, the arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type.

If I correctly understand this paragraph, it means the argument x which is of type int is implicitly converted to long when the function is called. According to 6.3.1.3 §1:

When a value with integer type is converted to another integer type other than _Bool, if the value can be represented by the new type, it is unchanged.

As int has a lower rank than long, every int variable can be represented by a long variable. Therefore, the argument x can be converted into a long. Therefore, this is not undefined behavior.

Which interpretation of the standard is right? Is my code undefined behavior or not?

Pierre
  • 1,942
  • 3
  • 23
  • 43
  • Would you expect `f(42)` to work? – Support Ukraine May 04 '21 at 09:05
  • @4386427 `42` is of type `int`. I want to say: "Yes, it should work!" but after reading the standard, I'm not sure anymore. – Pierre May 04 '21 at 09:09
  • Paragraph 6 seems to be mostly about *default argument promotions*, but the part about functions defined with a type that includes a prototype seems poorly worded, as it seems to imply that calling a function defined with a prototype that includes an ellipsis results in UB. – Ian Abbott May 04 '21 at 09:33
  • Implicit ranked conversion takes care of it, as a `long` can fully represent the binary value of an `int`. – Devolus May 04 '21 at 09:34
  • 2
    The key lies in *after promotion*. Therefore this is not UB. – Cheatah May 04 '21 at 09:37
  • @Cheatah An `int`, after promotion, stays an `int`. Therefore, after promotion, the argument still doesn't have the same type as the parameter. – Pierre May 04 '21 at 09:43
  • the UB would trigger is `f` were undefined or declared as `extern long f()` – tstanisl May 04 '21 at 09:45
  • 2
    @Pierre After promotion, the `int` argument becomes `long` as `long`'s rank is greater. And thus there's no incompatibility or UB in your example. – P.P May 04 '21 at 09:46
  • @P.P There is a difference between integer _promotion_ and integer _conversion_. A promotion cannot promote an integer to a rank higher than the rank of `int`, am I wrong? – Pierre May 04 '21 at 09:49
  • `which is of type int is implicitly converted to long when the function is called. According to 6.3.1.3 §1:` When something is converted, then it is converted, not, like, converted but only if rank. It __is__ converted, the end. – KamilCuk May 04 '21 at 09:52
  • In your first quote you write "[...]" cutting out the text that says (paraphrased) "bla bla. **Otherwise**, ..." so what you have written there says the opposite of what the standard actually says – M.M May 04 '21 at 12:07
  • @M.M The full quote is: "If the function is defined with a type that includes a prototype, and either the prototype ends with an ellipsis (, ...) or the types of the arguments after promotion are not compatible with the types of the parameters, the behavior is undefined." There is no _Otherwise_ in it. – Pierre May 04 '21 at 12:35
  • You just did it again. The structure is "If X, foo. If Y, bar" which you are abbreviating to "If X, [...] bar" – M.M May 04 '21 at 19:43

4 Answers4

2

You provided irrelevant quotes relative to your code snippet. According to the same section (6.5.2.2 Function calls)

2 If the expression that denotes the called function has a type that includes a prototype, the number of arguments shall agree with the number of parameters. Each argument shall have a type such that its value may be assigned to an object with the unqualified version of the type of its corresponding parameter.

The function f has a prototype that is visible in the call expression

extern long f(long x);

and this assignment

int argument;
long parameter;
parameter = argument

is correct.

As for this quote

6 If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions. If the number of arguments does not equal the number of parameters, the behavior is undefined. If the function is defined with a type that includes a prototype, and either the prototype ends with an ellipsis (, ...) or the types of the arguments after promotion are not compatible with the types of the parameters, the behavior is undefined. If the function is defined with a type that does not include a prototype, and the types of the arguments after promotion are not compatible with those of the parameters after promotion, the behavior is undefined, except for the following cases:

Then it means the following. The function calling expression does not see the function prototype. So the default argument promotions are performed. But somewhere else the function is defined with a function prototype and the promoted arguments are not compatible with function parameters. In this case you will have undefined behavior.

Here is a demonstrative program with undefined behavior related to a function call. The compiler can issue an error message.

#include <stdio.h>

void f();

int main(void) 
{
    short x = 10;
    
    f( x );
    
    return 0;
}

void f( char *s )
{
    printf( "s = %s\n", s );
}

or

#include <stdio.h>
#include <limits.h>

void f();

int main(void) 
{
    unsigned int x = UINT_MAX;
    
    f( x );
    
    return 0;
}

void f( int x )
{
    printf( "x = %hd\n", x );
}

For example in the last program the argument x in the call expression

f( x );

is promoted to the type unsigned int. But according to the function definition the function expects an argument of the type signed int and the passed value can not be stored in the type signed int. So the behavior is undefined. But your original example of a function call is not related to this quote.

Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335
  • The paragraph 2 you are quoting and the paragraph 6 seems opposing, aren't they? – Pierre May 04 '21 at 09:13
  • Re “and the passed value can not be stored in the type `short`”: The value is irrelevant; it is sufficient that the types be wrong for the behavior to be undefined. E.g., if an `int` value of 1 were passed, the `short` parameter could get the wrong value due to endianess issues (`int` argument pushes four bytes onto stack, `short` parameter looks at only two, and they are the high-value bytes, and this also displaces other argument/parameter matches). So the program would “misbehave” even though `1` is representable in both `int` and `short`. – Eric Postpischil May 04 '21 at 11:45
  • @EricPostpischil I mean the case when the parameter has for example signed type and the argument has a corresponding unsigned type and vice versa and as a result the valure can not be represented in the parameter.. – Vlad from Moscow May 04 '21 at 11:49
1

The part "the arguments after promotion" is confusing, it refers to the default argument promotions defined earlier on in that same paragraph. Which doesn't apply here, since those rules are only used when there is no prototype or when we have variadic functions.

So "arguments after promotion are not compatible with the types of the parameters" applies to cases where you don't have a prototype, apply the default argument promotions (integer promotion in case of integers) and if the types are not compatible then, there is undefind behavior.

But since you have a prototype, forget about default argument promotion, instead continue to read the next part, C17 6.5.2.2/7 emphasis mine:

If the expression that denotes the called function has a type that does include a prototype, the arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type.

Then we go read what's said about "as if by assignment", C17 6.5.16 emphasis mine:

the left operand has atomic, qualified, or unqualified arithmetic type, and the right has arithmetic type;

Both int and long are arithmetic types (and there are no qualifiers), this is a valid form of assignment. Further down in the same chapter:

The type of an assignment expression is the type the left operand would have after lvalue conversion.

So basically the code passing the parameter is equivalent to simple assignment:

int x;
long y;
y = x;

If we let the standard send us further on this merry chase, next look up lvalue conversion, C17 6.3.2.1:

...an lvalue that does not have array type is converted to the value stored in the designated object (and is no longer an lvalue); this is called lvalue conversion.

And then the actual conversion for integer types, C17 6.3.1.3:

When a value with integer type is converted to another integer type other than _Bool, if the value can be represented by the new type, it is unchanged.
Otherwise, if the new type is unsigned, the value is converted by repeatedly adding or subtracting one more than the maximum value that can be represented in the new type until the value is in the range of the new type.
Otherwise, the new type is signed and the value cannot be represented in it; either the result is implementation-defined or an implementation-defined signal is raised.

A long can always hold the value of an int, so the first sentence is the conversion that applies in this case.

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • I'm confused about 6.5.2.2/6. If I correctly understand you, you are saying that this paragraph is only about functions with a type that does not include a prototype. However, later in the paragraph, it is said: "If the function is defined with a type that includes a prototype, ...". It seems that this paragraph talks about functions without prototype and functions with prototype, isn't it? – Pierre May 04 '21 at 10:04
  • @Pierre Yes, it is very badly written. "includes a prototype, **and** either the prototype ends with an ellipsis (, ...) **or** the types of the arguments after promotion". It's not obvious what these "and" and "or" refer to. Does it only mean prototypes with ellipsis? It often helps to read the standard like this: §6 "If the expression that denotes the called function has a type that **does not include a prototype** ... [irrelevant text here] §7 If the expression that denotes the called function has a type that **does include a prototype**, [relevant text here] " – Lundin May 04 '21 at 10:22
0

Code is correct. IMO, the first interpretation does not a apply.

It is actually referring to calling a function without a prototype that is defined with a prototype:

long g(int x)
{
    return f(x);
}

// other translation unit
long f(long x) {
    return 0;
}

The code is defined only if f is called with single argument of type compatible with long.

tstanisl
  • 13,520
  • 2
  • 25
  • 40
  • Note this is ill-formed since C99, calling a function without even a declaration – M.M May 04 '21 at 12:10
  • @M.M, where is it forbidden? "Annex I" mentions only a warning. – tstanisl May 04 '21 at 12:17
  • 1
    "Annex I" is non-normative. 6.5.1 makes `f(x)` a syntax error: identifiers that have not been declared are not *primary-expression*, and the syntax for function call operator requires a *primary-expression*. C89 had the same text but also had an exception in the section on function calls that an undeclared identifier behaved as if there were implicit declaration. That exception was removed in C99 – M.M May 04 '21 at 12:29
  • @M.M: The Standard does allow functions to be declared without prototypes. I've worked with some C-dialect implementations which used different naming and calling conventions for prototyped functions that accept arguments versus those that don't. While I don't think the Standard would regard such processing as conforming, it would be useful if a project needs to link a translation unit written using old-style declarations and definitions. – supercat May 05 '21 at 21:20
  • @supercat I am referring to using undeclared identifiers, not to non-prototype declarations – M.M May 05 '21 at 21:48
  • @M.M: While C99 would require the addition of some kind of declaration, adding a non-prototype declaration to the example would allow it to show the same point as it would have in C89 with no prototype. – supercat May 06 '21 at 20:05
0

In a typical C implementation where compilation units are processed independently, and the linker combines the separately-compiled object files and performs address relocation without attempting other optimization, there will be a specification, which today would commonly be called the Application Binary Interface, which describes among other things where/how code which calls a function should store the arguments, and where/how a function should expect to find arguments stored by its caller.

If a function attempts to retrieve arguments in a manner inconsistent with how its caller had stored them, the results are not likely to be meaningful. On the other hand, many platform ABIs describe behavior in terms of storage formats, rather than C data types. Thus, on e.g. a 32-bit ARM implementation where both int and long are 32-bit data types, a function that expects an int would be called in exactly the same fashion as one that expects a long or an int32_t. An ARM implementation that processes the code in different compilation units independently would thus not need to care if one compilation unit uses type int and another uses long, provided that both types have the same representation.

In general, one should make parameter/argument types match when practical, even on implementations that wouldn't care about such things, because it will make it easier for people to read the code and know what it's doing. In some situations, however, one may have different compilation units that expect to be given pointers to functions whose arguments are of different types with matching representations. In such situations, if one is using an implementation that processes compilation units separately, it may be possible to pass the address of a single function to code in both compilation units. Unfortunately, there is no means by which a programmer can indicate when a function call needs to be handled in a fashion consistent with the ABI, without regard for whether the Standard would define its behavior, and some aggressive optimizers make no attempt to meaningfully process constructs whose behavior would be defined by the ABI but not the Standard.

supercat
  • 77,689
  • 9
  • 166
  • 211