4

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.

10
  • Related topic, but I can't dig the clear answer to my question out of it. Commented Jul 24, 2024 at 10:57
  • 2
    Your linked page explains this at some length starting from Definition w/o Prototype. What specifically didn't you understand? Commented Jul 24, 2024 at 11:00
  • Specifically, why do I need to use 4 ever? I want to use 3 always, will I run into troubles? Commented Jul 24, 2024 at 11:01
  • 1
    It's good practice to make your function declarations and definitions match. For that reason, it's best to avoid (3). Ideally, f() would mean what f(void) means, but for historical reasons, f() indicates a missing prototype. In the case of a function definition, it doesn't really matter, but for a function declaration it does. Commented Jul 24, 2024 at 11:14
  • 1
    @Useless It changed exactly with C23. (One of several reasons why I don't like that revision. Though mostly obsolete indeed, prototypeless decls in <C23 can still be used to get useful otherwise unavailable functionality out of C: stackoverflow.com/questions/1749233/…) Commented Jul 24, 2024 at 11:55

2 Answers 2

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.

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

4 Comments

Thanks for explanation, learned something new. My decision is simple: use 2 and 4 always.
Have you considered 6.7.6.3p14? "… An empty list in a function declarator that is part of a definition of that function specifies that the function has no parameters. …".
@IanAbbott: Yes, and what consequence follows from that? As I wrote, the function has no parameters. The difference between (3) and (4) is in the type information associated with the identifier (the function name). The identifier and the function are different things.
I thought that void f{) { } would declare the function being defined as having no parameters the same as void f(void) { }, but I was mistaken. 6.7.6.3p14 only applies to the function definition itself and not to the declaration of the function that is seen by calls where the function definition is in scope and there are no other declarations of the function in scope that include a prototype. E.g. void f() {} void g(void) { f(1); } has no constraint violation, but void f(void); void f() {} void g(void) { f(1); } has a constraint violation (so requires a diagnostic). Both have UB.
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.

2 Comments

I beg to differ. Definition 3 is crystal clear, especially to the experienced C programmers that you would expect to find on a "serious project". That the function definition might be wrong because "the programmer [...] forgot to return to finish the job" is irrelevant, as the question is about what the definitions mean, as given. On the other side, no, the discussion is not equally valid for declarations 1 and 2. In all versions of C to date (but not in the upcoming next version) 1 and 2 are in fact semantically different.
@JohnBollinger, I've rolled back the wrong statement. Please don't discuss here 1 and 2. There are dozens of topics already, really! ;)

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.