I've been reading about function pointer compatibility, but have not found the following scenario documented as being acceptable (below).
With this code, it is allowed (without warnings) to call a function pointer with a parameter, even-though it is defined as having an empty parameter list. Also, a function with a single pointer as a parameter list is allowed (without warnings) to be assigned to the function pointer with type defined as having an empty parameter list. And the subsequent call with a parameter is allowed.
Since the caller is responsible for cleaning up the parameters on the stack, I suppose this is memory-safe, no? There's just something about this that makes me doubt its validity.
Does the code below compile and run without warnings because it is actually valid C? Are these function types indeed compatible, and if so, is this defined behavior?
Program
#include <stdlib.h>
#include <stdio.h>
void functionA()
{
printf("A\n");
}
void functionB(uint8_t* parameter)
{
printf("B %d\n", *parameter);
}
void (*functionPointer)();
int main()
{
uint8_t number = 42;
functionPointer = functionA;
functionPointer(&number);
functionPointer = functionB;
functionPointer(&number);
//functionA(&number); // warning: too many arguments in call
return 0;
}
Output
% ./a.out
A
B 42
Under C17 and earlier, functionPointer
is defined as a pointer to a function taking an unspecified number of arguments and returning void
. Such a function pointer is compatible with any function that returns void
.
The C11 standard gives the following description for determining if two function types are compatible, section 6.7.6.3p15:
For two function types to be compatible, both shall specify compatible return types. Moreover, the parameter type lists, if both are present, shall agree in the number of parameters and in use of the ellipsis terminator; corresponding parameters shall have compatible types. If one type has a parameter type list and the other type is specified by a function declarator that is not part of a function definition and that contains an empty identifier list, the parameter list shall not have an ellipsis terminator and the type of each parameter shall be compatible with the type that results from the application of the default argument promotions. If one type has a parameter type list and the other type is specified by a function definition that contains a (possibly empty) identifier list, both shall agree in the number of parameters, and the type of each prototype parameter shall be compatible with the type that results from the application of the default argument promotions to the type of the corresponding identifier. (In the determination of type compatibility and of a composite type, each parameter declared with function or array type is taken as having the adjusted type and each parameter declared with qualified type is taken as having the unqualified version of its declared type.)
The sections in bold are what applies here. So assigning either functionA
or functionB
to functionPointer
is well defined.
The problem however is how functionA
is called through this pointer:
functionPointer = functionA;
functionPointer(&number);
Section 6.5.2.2p2 states the following constraint regarding function calls:
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.
Since the above is calling functionA
with the wrong number of parameters, this is a constraint violation and the code therefore has undefined behavior.
Starting with C23, the type of functionPointer
is a pointer to a function that takes no parameters and returns void
. This is specified in section 6.7.6.3p13 of the C23 standard
For a function declarator without a parameter type list: the effect is as if it were declared with a parameter type list consisting of the keyword
void
. A function declarator provides a prototype for the function
So functionPointer = functionA
will likely generate a warning for an incompatible pointer conversion, and functionPointer(&number)
will generate an error for calling a function with the wrong number of parameters.
A declaration of a function that includes the type of its parameters is called a prototype (C 2018 6.2.1 2).
The rules regarding function types without parameter declarations are expected to change in C 2023. C 2023 is expected to remove non-prototype declarations from the standard and make ()
in a function declaration equivalent to (void)
, so the code in the question will be non-conforming, making the questions asked largely moot.
The remainder of this answer addresses C 2018, which is largely unchanged in this regard from C 1999.
I've been reading about function pointer compatibility, but have not found the following scenario documented as being acceptable (below).
Calling a function that is defined with a prototype using a expression that does not have a prototype is specified in C 2018 6.5.2.2 6. (“Expression” is used here rather than referring merely to a function name because a function can be called using a function name, a variable that is a pointer to a function type, or an expression that is a cast resulting in a pointer to a function type.) Calling a function that is not defined with a prototype using an expression that does have a prototype is specified in the following paragraphs.
With this code, it is allowed (without warnings) to call a function pointer with a parameter, even-though it is defined as having an empty parameter list. Also, a function with a single pointer as a parameter list is allowed (without warnings) to be assigned to the function pointer with type defined as having an empty parameter list. And the subsequent call with a parameter is allowed.
Non-prototype function types essentially mean the parameters are not specified in the type. The rules about mixing uses of non-prototype function types and prototype function types are largely that, as long as the arguments used to call the function are compatible with what the function actually expects, the behavior is defined. There is some additional tolerance, such as allowing passing an int
for an unsigned int
provided the value is representable in both.
Since the caller is responsible for cleaning up the parameters on the stack, I suppose this is memory-safe, no?
The C standard says nothing about how is responsible for cleaning up arguments on the stack and can be implemented on platforms that require the calling to clean up arguments and on platforms that require the called function to clean up arguments.
In any function call, the compiler knows the arguments passed from the actual arguments, regardless of whether the calling expression type has a prototype or not. So the compiler could generate code for the calling routine to clean up the arguments, if that is what the platform requires.
In a function definition with a prototype without ...
, the compiler knows the parameters from the prototype. In a function definition without a prototype, the parameters are declared in “identifier list” style: The parameter names are listed inside parentheses, and their declarations following the closing parentheses of the function parameters. So, in both of these cases, the compiler knows the parameters and hence the expected arguments and could generate code to clean up the stack if that is what the platform requires. This includes the case where a function is defined with ()
—in a function definition, this means there are no parameters and hence no arguments. (In function declaration that is not a function definition, ()
means the parameters are not specified.)
This leaves function definitions using ...
. In this case, the compiler generally cannot know the arguments. Further, the arguments are not necessarily known to the calling routine. Although printf
, for example, has a format string that instructs it about arguments to expect, it is valid to pass printf
more arguments than the format string calls for (C 2018 7.21.6.1 2). On a platform where the caller is responsible for cleaning up the stack, this is no problem, as the caller knows what arguments were passed. On a platform where the calling function is responsible for cleaning up the stack, this can be implemented by interface rules that require the calling routine to pass information about the arguments. This would be “hidden” information that is not visible in the arguments. For example, the interface may require the calling routine to pass the number of bytes to remove from the stack when returning from the function call.
Does the code below compile and run without warnings because it is actually valid C?
More precisely, it may compile and execute without warnings because it is conforming C code. (“Valid” has a different meaning and is not defined in the C standard. It is valid for programs to use extensions to the C language.) However, it is not strictly conforming C code. Because the first call using functionPointer
passes an argument to a function defined with no parameters, it violates C 2018 6.5.2.2 6: “… If the number of arguments does not equal the number of parameters, the behavior is undefined…” This makes the behavior not defined by the C standard.
Note the difference between functions defined with and without ...
. Although we can pass printf
arguments it does not know about, the standard does not guarantee we can do that for functionA
. The reason is that ...
puts the compiler on notice that it must, if the platform requires it, use the additional argument information discussed above. Without the ...
, the compiler may generate code expected a fixed number of arguments with fixed types, and so that would be wrong when called with a different number of arguments.
Are these function types indeed compatible, and if so, is this defined behavior?
Yes. Rules for compatibility of function types are specified in C 2018 6.7.6.3 15, and they allow prototype types to be compatible with non-prototype types. For types in C, “compatible” largely means that two types can be completed to be the same type, i.e., that any portions of them that are specified are the same. For example, an array type with a size (number of elements in the array) is compatible with an array type without a size (number is not specified). Similarly, two function types that have the same return type but differ in that one specifies the parameter types and the other does not are compatible.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With