cpointersparameter-passingfunction-pointersfunction-prototypes

Using function pointers with empty parameter lists in C


I have seen a lot of C code using pointers to functions with an empty parameter list so that they can be used to call functions with different prototypes without a cast, so long as the return type is the same.

For example:

#include <stdio.h>

typedef void (*func_returning_void) ();

void empty_args ()
{
    printf("empty args\n");
}

void zero_args (void)
{
    printf("zero args\n");
}

void one_int_arg (int arg)
{
    printf("one arg: %d\n", arg);
}

void two_string_args (const char *arg1, const char *arg2)
{
    printf("two args: %s %s\n", arg1, arg2);
}

int main (void)
{
    func_returning_void funcptr;

    funcptr = empty_args;
    funcptr();
    funcptr = zero_args;
    funcptr();
    funcptr = one_int_arg;
    funcptr(1);
    funcptr = two_string_args;
    funcptr("one", "two");
}

So long as arguments' number and types match the parameter list of the function being called, is this technique well-defined, or does it rely on undefined behavior ?

Note: I know that the other way around is undefined behavior, because in a function definition, the empty parameter list () has the same meaning as (void) i.e. exactly zero arguments; as the C standard states:

"An empty list in a function declarator that is part of a definition of that function specifies that the function has no parameters."

void empty_params() {}

int main (void)
{
    void (*funcptr) (int) = empty_params; // UB
    funcptr(1234);
}

Solution

  • The applicable paragraph in the 2018 standard is 6.5.2.2 6, because it starts “If the expression that denotes the called function has a type that does not include a prototype.” (A prototype is a declaration of a function that declares the types of its parameters. So typedef void (*func_returning_void) (); does not include a prototype.) This is expected to change in the 2023 standard, which removes support for function declarations without prototypes. () for function declarations will become the same as (void).

    Regarding calling a function that is defined with a prototype, this paragraph says:

    … 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…

    Thus, when calling a function defined with a prototype using a function pointer declared with (), the behavior is defined as long as the number of arguments matches the number of parameters, the types after argument promotion are compatible, and the definition does not use an ellipsis.

    Note that the behavior of your two_strings_args example is not defined by the C standard. funcptr("one", "two"); calls the function with arguments of type char *, whereas the function is defined with parameters of type const char *, and these are not compatible types.