Search code examples
clanguage-lawyerc99c23

Function pointer compatibility between single pointer and empty parameter lists


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

Solution

  • 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.