Question

Function definition with prototype vs without prototype

There are (source):

void f();            // declaration (1)
void f(void);        // declaration with prototype (2)
void f() { ... }     // definition (3)
void f(void) { ... } // definition with prototype (4)

What is the difference between 3 and 4? The source doesn't explain that difference and to me 4 looks redundant.

 4  88  4
1 Jan 1970

Solution

 3

The functions created by (3) and (4) are identical—both have no parameters, and the compiler may generate identical code for them. However, in C 2018 (still the standard at this moment) the type information attached to the function name differs, and this can affect calls to the function. C 2024 is expected to change this, making (3) equivalent to (4).

Assuming there is no other visible declaration of f that modifies the declaration, then calls to the f declared in (4) are subject to this constraint in C 2018 6.5.2.2 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…

The C standard requires a constraint violation to be diagnosed; if a program contains a constraint violation, a compiler must produce a diagnostic message. In contrast, calls to the f declared in (3) are not subject to that constraint, and a compiler is not required to produce a diagnostic message (although it may do so).

Additionally, after (3), the compiler analyzes a call to f using the rules in C 2018 6.5.2.2 6:

If the expression that denotes the called function has a type that does not include a prototype,…

After (4), the compiler analyzes a call to f using 6.5.2.2 7:

If the expression that denotes the called function has a type that does include a prototype,…

However, there are generally no consequences of this because these paragraphs specify only two things:

  1. When the expression denoting the called function does not have a prototype, the behavior is undefined if the number of arguments does not equal the number of parameters.

  2. How the arguments are converted in preparation for the call.

If there are no arguments, then 2. does not apply. If there are arguments, then the behavior is undefined in both cases, and I do not see any opportunity for a different conversion of the arguments to cause any meaningful difference in behavior.

Supplement

Interestingly, although the standard does not require a compiler to diagnose when the function in (3) is called with a parameter, it does require a compiler to diagnose when it is redeclared with a parameter, such as void f(int x);. C 6.7 2 has this constraint, requiring diagnosis:

All declarations in the same scope that refer to the same object or function shall specify compatible types.

and the specification for compatible function types in 6.7.6.3 15 says:

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

Since void f(int x) has a parameter type list, and void f() {} contains an empty identifier list, they must, to be compatible, agree in the number of parameters. They do not agree, so they are not compatible, so the constraint violation must be diagnosed.

It seems like this is an oversight in the standard; since this constraint compels the compiler to retain knowledge of the number of parameters in a function definition, even if it uses an identifier list and not a prototype, the standard could also have required compilers to diagnose calls with an incorrect number of parameters without requiring the compiler to retain any additional information about the function.

However, that would be useful only in situations where the function definition were visible at the point of the call, which is often not the case, as the function may be defined in another translation unit or later in the same unit. So perhaps not an important case to cover. Nonetheless, it could have caught some bugs.

2024-07-24
Eric Postpischil

Solution

 0

I might not provide the most scientific answer, but here is my attempt.

For a beginner, definitions 3 and 4 are identical. However, in more serious projects, not only the present information matters (and needs to be verified), but the missing information too.

So the bottom line is like this:

  • definition 4 clearly states that the programmer made the explicit decision to have a function without arguments;
  • definition 3 tells nothing clearly; it might be that the function must run without formal parameters, or it means that the programmer wrote the body of the function before writing the exact list of formal parameters, and forgot to return to finish the job.

To make matters worse, if the local variables have the same name of some global variables, the compiler will accept the function without any message - and then, at some time later, you will discover the fun of debugging.

That is why I prefer to always make things explicit. If something is missing (not explicit), I know that I have some more work to be done.


An "extreme" variant of the explanation is like this:

  • function 3 has no parameters at all;
  • function 4 has one parameter, and that parameter specifies that there are no parameters.

Again, the same conclusion is obvious: function 3 does not clarify the expectations / needs.


There is a more hidden aspect to the difference, and that is even more (potentially) hurting to the beginners. NOT using "void" as a parameter list "teaches" the beginner programmer that "void" is equivalent to "nothing", or rather the opposite - that "nothing" is the same with "void".

However, as you can easily notice, the return type of the function can also be "void" or "nothing". But in this case, the "nothing" (as the return type of the function) no longer means "void", but it actually (literally) means "int". Again, a funny source of debugging activities and white hairs.

2024-07-24
virolino