3

I'm trying to reconcile 2 things.

I know that a function signature TYPE func(void); means the function accepts no arguments, which is different than: TYPE func();, which means the function accepts any arguments (and this is difference in C vs. C++).

I also know that function pointers are not compatible with functions that have different arguments (different signatures) (sorry if I'm phrasing this wrong).

I have read that you can cast a function pointer to a different type, and then back again, and then you can call the function (i.e. it works "round trip").

However, I am interested in what happens when calling functions that accept different arguments through a pointer. I know this probably falls under undefined behavior. But what I find strange is the notion that the function accepts "any arguments". Maybe this interpretation is wrong.

But I'll explain now with an example.

#include <stdio.h>
#include <stdbool.h>

void (*func)(); /* accepts any arguments */

void func_1(int x);          /* 1 argument */
void func_2(int x, float y); /* 2 arguments */

enum MODE {PRINT_THEM, NOTE_THEM} mode;

int main(void)
{
    printf("Printing...\n");
    mode = PRINT_THEM;
    func_1(8);
    func_2(9, 10.0F);

    printf("Noting...\n");
    mode = NOTE_THEM;
    func = func_1;
    func(); /* function can have any arguments, so this one has 1, but it's not used */

    func = func_2;
    func(); /* function can have any arguments, so this one has 2, but they are not used */

    return 0;
}

void func_1(int x)
{
    if (mode == NOTE_THEM)   
        printf("func_1() has 1 argument: (int)\n");
    else /* mode == PRINT_THEM */
        printf("x == %d\n", x);
}

void func_2(int x, float y)
{
    if (mode == NOTE_THEM)   
        printf("func_2() has 2 arguments: (int, float)\n");
    else /* mode == PRINT_THEM */
        printf("x == %d, y == %f\n", x, y);
}

The overall idea here is when mode == PRINT_THEM, you use the arguments, and therefore the arguments matter, and you can't use the function pointer.

However when mode == NOTE_THEM, you don't use the arguments, and therefore their particular values don't matter (they could be anything, even a garbage value would be fine). In this case, since the arguments don't matter, ideally the function pointer that accepts any arguments (in this case no arguments) could be used.

The thing I want to avoid is being forced to use some kind of placeholder arguments, when they don't matter. For example:

mode = NOTE_THEM;
func_1(placeholder_int);
func_2(placeholder_int, placeholder_float);

The only solution I can think of actually does not involve function pointers at all, but you could use C99 compound literals, and rely on default initialization, in order to call the function with the right arguments (satisfy the compiler), but not care about their specific values or what they specifically are. Kind of like this:

mode = PRINT_THEM;
func_1( (FUNC_1_ARGS){8} );
func_2( (FUNC_2_ARGS){9, 10.0F} );

mode = NOTE_THEM;
func_1( (FUNC_1_ARGS){} );
func_2( (FUNC_2_ARGS){} );
8
  • Related: Unspecified number of parameters in C functions - void foo() Commented Dec 15, 2023 at 15:04
  • 3
    "TYPE func();, which means the function accepts any arguments" isn't quite right. In previous editions of C, func(); declared a function that takes a not-yet-specified number of arguments. However, the corresponding function definition would need to specify a fixed number of arguments. This was mostly for compatibility with old-style K&R function definitions. Attempting to call a function with just a func(); declaration is UB. This was also done away with entirely in C23. Nowadays, func(); is exactly equivalent to func(void); Commented Dec 15, 2023 at 15:10
  • See also en.cppreference.com/w/c/language/function_declaration. Note the description of syntax (3). Commented Dec 15, 2023 at 15:11
  • 1
    That's mostly right, @Brian61354270, but in those C versions that permit function declarations that do not provide a prototype, it does not inherently invoke undefined behavior to call a function declared (only) that way. The rules for argument conversion are different depending on whether the function has an in-scope prototype, but as long as the argument list is properly matched with the function definition, such calls have well-defined behavior. Commented Dec 15, 2023 at 17:29
  • 1
    The parameter list (...) is invalid. There needs to be at least one non-variadic parameter before the , .... Commented Jan 2, 2024 at 10:44

2 Answers 2

4

Your problem is right at the beginning:

I know that a function signature TYPE func(void); means the function accepts no arguments,

Yes.

which is different than: TYPE func();,

Yes, prior to C23. Starting in C23, TYPE func(); means the same thing as TYPE func(void);, just as it does in C++.

which means the function accepts any arguments (and this is difference in C vs. C++).

NO. Prior to C23, TYPE func() declares a function that accepts a particular but unspecified number of arguments, of particular but unspecified types. The fact that the declaration does not provide a prototype for the function does not relieve callers from calling it with the appropriate number of arguments, having appropriate types. Additionally, whatever arguments are provided in a given call are subject to the default argument promotions, as opposed to being subject to assignment conversion to the parameter types given in a prototype.

However, I am interested in what happens when calling functions that accept different arguments through a pointer. I know this probably falls under undefined behavior.

No "probably" about it. Details differ for functions defined with prototypes and those defined without (K&R style), but calling a function with an argument list that is incompatible with the function's definition explicitly produces UB.

But what I find strange is the notion that the function accepts "any arguments".

I guess your intuition is serving you well. Such a notion is foreign to C, except in the form of variadic functions, though some other languages support it (Python, Bash, ...).

The overall idea here is when mode == PRINT_THEM, you use the arguments, and therefore the arguments matter, and you can't use the function pointer.

However when mode == NOTE_THEM, you don't use the arguments, and therefore their particular values don't matter

As far as the language specifications are concerned, it's not about whether the arguments are used. The function call itself has undefined behavior if the arguments are not properly matched to the function. You can make it difficult or impossible for the compiler to do compile-time argument checking, and you can affect what argument conversions are applied at the function call interface, but you cannot escape the requirement to call the function via a compatible function pointer, with arguments of types that are correctly matched to the function definition.

Sign up to request clarification or add additional context in comments.

1 Comment

Excellent answer! I'll just add that the latest published version of the C standard is C17 (C18), but C23 is something to bear in mind for the future (which I guess will be sometime in 2024…).
3

Can a function pointer that accepts any arguments be compatible with functions that accept different arguments

Your “function pointer that accepts any arguments” is actually, as the C standard describes it, a pointer to a function type without a prototype. It is more accurate to say the arguments the function accepts are unknown (in the type), not that it accepts any arguments.

(A function prototype is a declaration of a function that declares the types of its parameters.)

A function type without a prototype is compatible with a function type with a prototype, per C 2018 6.2.7 3. Also, pointers to such types are compatible. However, compatibility alone is not sufficient to make the call defined. There are additional rules for calling functions in C 2018 6.5.2.2.

Your code has calls that use a type without a prototype to call functions that are defined with prototypes (parameter type lists). This is covered in 6.5.2.2 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…

There are additional rules in that paragraph I do not discuss here, as the above is sufficient: The number of parameters in the actual function definitions do not equal the number of arguments in the call, so the behavior is not defined by the C standard.

One reason for this is that not all calling conventions work if a function is called with a different number of arguments than it is expecting. In modern systems, we generally see that a function is passed its first few integer or pointer arguments in general registers. If you pass one, two, or three arguments, you put their values in registers, say, r3, r4, and/or r5. If you do not pass these arguments, the registers are left alone and have whatever values they have, but the computer state is otherwise the same. Notably, there is no difference in the stack frame. However, in older systems, the called function may have been expected to clean up the stack in certain ways when it was returning. If it had three two-byte parameters, it would pop six bytes from the stack in the process of returning. If the caller had not actually pushed six bytes on the stack, this mismatched pop would corrupt the stack pointer, and program execution would go awry.

So, C does allow some slack for playing with function types, but the rules basically are that the way you call a function must match the way the function is defined.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.