Declaring, Defining and Prototyping Functions in C

A function declaration serves as a hint to the compiler that a particular function exists. The function itself might be defined either in a different compilation unit or later on in the same compilation unit. The latter was especially useful in the old days where parsing was expensive and you wanted to avoid to parse a file several times. Let us have a look at an example:

int foo(void) {
    return bar(42);
}

int bar(int x) {
    return x+1;
}

Function foo calls bar which is defined after foo. If a compiler starts parsing the file from top to bottom, then as soon as code should be emitted for the expression bar(42) it is unclear how that code should look like. Does function bar expect as a first parameter a short, an int, or even a long—in other words what size should the first parameter have?

Function Declarators to the Rescue?

tl;dr   A function declarator which is not in prototype-format is still problematic

A function declarator defines a function name and may define information about the types and numbers of the corresponding function parameters. A function declaration consists of a function declarator and a return type. Likewise, a function definition consists of a function declarator, a return type, and additionally the body of the function.

That means in a function declaration and definition a declarator may omit information about the types and numbers of parameters. Only a declarator which is in prototype-format contains information about the number and types of parameters—excluding parameters which are given via trailing ellipses. Thus the following combinations are possible:

Declaration w/o Prototype

A function declaration without a prototype only introduces the name of a function and its return type. Nothing is known about the arguments of the function:

void foo();

This is indicated by an empty argument list. Note this does not define a nullary function. We will see below how to define a nullary function.

Since nothing is known about the arguments the compiler cannot warn if the function is called with an inappropriate number or types of arguments—which results in undefined behavior.

Declaration with Prototype

A function declaration with a prototype introduces in addition to the name and return type of a function (like a function declaration without a prototype does), also the number and types of all arguments (of course, excluding variadic arguments given via ellipses …):

void  foo0(int x);
float foo1(int, double);
short foo2(float x, bool, ...);
float foo3(void);

Hence, a function prototype is in contrast to a function declaration without a prototype more strict in the sense that it also requires the number and types of all arguments. This enables a compiler to warn if a function is called with a wrong number of arguments or with a wrong argument type.

Observe a nullary function is given by the single argument void (in contrast to C++ where no void argument is required).

Definition w/o Prototype

A function definition without a prototype consists of the return type, name and body of the function:

int foo() { return 42; }

Such functions are essentially nullary functions.

We will later on see why such function definitions without a prototype are still problematic and where the C standard is not stringent.

Definition with Prototype

A function definition including a prototype introduces the return type, name, the number and types of all arguments of a function.

int foo(short x) { return x + 42; }

Note, whenever a function has at least one argument, then a function definition includes a prototype.

The Good Old Days

Observe that we did not take care of the notation as introduced by Kernighan and Ritchie, i.e., we excluded functions of the form

main(argc, argv)
char *argv[];
{
    return 0;
}

where you could even omit type declarations which default to int. In the example the return type and the type of the first argument argc of the function main default to type int. Despite this in K&R notation it is allowed to make use of an identifier list and a separate parameter type list which is very uncommon these days. Hence we ignore this kind of notation.

Function Definition Without a (Prior) Prototype

If no prototype is available for a called function, then the compiler cannot check if the number and types of all arguments are compatible with the function. Hence the compiler has to assume that the code is correct as it is written down. Consider the following example:

void foo();
void foo() { }

int main(void) {
    foo();
    foo(42);
    return 0;
}

The declaration and definition of function foo do not contain a prototype, respectively. In such a case the C11 standard assumes that the function is a nullary function.

C11 § 6.7.6.3 Function declarators (including prototypes) ¶ 14
[…] An empty list in a function declarator that is part of a definition of that function specifies that the function has no parameters. […]
An identifier list declares only the identifiers of the parameters of the function. An empty list in a function declarator that is part of a definition of that function specifies that the function has no parameters. The empty list in a function declarator that is not part of a definition of that function specifies that no information about the number or types of the parameters is supplied.

The C standard is at that point vague. What does the function has no parameters mean? Does this mean that the type of function foo is compatible with type f when typedef void f(void);? Does the function definition also define a prototype which is used in the subsequent lines of the very same compilation unit? For the latter question, let us check what GCC outputs. If compiled via GCC 8.1.1 using the options -Wall -Wextra -pedantic -std=c11 no warning or error is raised, whereas, Clang 6.0.0 outputs the following warning:

test.c:6:11: warning: too many arguments in call to 'foo'
    foo(42);
    ~~~   ^
1 warning generated.

Since GCC does not even output a warning and Clang only outputs a warning, I assume that the standard is to read that no prototype is given in the function definition.

A function definition can, however, include a prototype as shown in the following:

void foo();
void bar(void) { foo(42); } // Compiles
void foo(void) { }
void baz(void) { foo(42); } // ERROR

In line one a function named foo is declared with return type void. Since no arguments are specified, the call to the function at line two is somewhat fine, i.e., at this point in time the best the compiler can do is to assume that the call is correct. At line three function foo is finally defined and also a prototype is given implicitly by the definition. From this point on to the end of the current compilation unit, the prototype is in scope. Therefore, at line four the compiler detects a misuse of function foo since it is called with a parameter whereas the prototype specifies that the function has no arguments at all. Hence, a compile time error is raised.

C11 § 6.9.1 Function definitions ¶ 7
The declarator in a function definition […] also serves as a function prototype for later calls to the same function in the same translation unit. […]
The declarator in a function definition specifies the name of the function being defined and the identifiers of its parameters. If the declarator includes a parameter type list, the list also specifies the types of all the parameters; such a declarator also serves as a function prototype for later calls to the same function in the same translation unit. If the declarator includes an identifier list,163) the types of the parameters shall be declared in a following declaration list. In either case, the type of each parameter is adjusted as described in 6.7.6.3 for a parameter type list; the resulting type shall be a complete object type.

Function Definition With a Prototype

So everything is fine as long as a prototype is given whenever a function is called? Well, yes and no. You are on the save side, if the code compiles. But there is a quirk which is surprising, if the code does not compile. Consider the following code:

void foo();
void foo(int x)   { } // Compiles

void bar(float x) { } // Compiles

void baz();
void baz(float x) { } // ERROR

All three functions come with a prototype at their definition site, respectively. The functions foo and baz have in addition a prior function declaration without a prototype. The functions foo and bar compile whereas function baz does not, i.e., a compile time error is raised.

Why? Long story short the problem here is the combination of an argument type which gets promoted and a function declaration without a prototype.

Lets get through this piece by piece. Whenever a function is called for which no prototype is available, then all arguments get promoted according to C11 section 6.5.2.2 paragraph 6:

C11 § 6.5.2.2 Function calls ¶ 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 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. 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. If the function is defined with a type that does not include a prototype, and the types of the arguments after promotion are not compatible with those of the parameters after promotion, the behavior is undefined, except for the following cases:
— one promoted type is a signed integer type, the other promoted type is the corresponding unsigned integer type, and the value is representable in both types;
— both types are pointers to qualified or unqualified versions of a character type or void.

Lets illustrate the problematic part by an example:

void baz();

int main(void) {
    baz(42.0f); // float -> double
    return 0;
}

void baz(float x) { }

When function baz gets called no prototype is available. Therefore, the argument gets promoted from type float to double. However, typically both types are not compatible and they differ in size (float: 32 bit, double: 64 bit usually). Hence at this point no valid code can be omitted and the compiler must return with an error in order to be sound.

Lets get back to function baz of the previous example. Prior the function definition a function declaration without a prototype is given. In such a case the C11 standard requires that each type of an argument of the function definition is compatible to its corresponding type after the default argument promotions.

C11 § 6.7.6.3 Function declarators (including prototypes) ¶ 15
For two function types to be compatible […]. If one type has a parameter type list and the other type is specified by a function declarator that is not part of a function definition and that contains an empty identifier list, the parameter list shall not have an ellipsis terminator and the type of each parameter shall be compatible with the type that results from the application of the default argument promotions. […]
For two function types to be compatible, both shall specify compatible return types.146) Moreover, the parameter type lists, if both are present, shall agree in the number of parameters and in use of the ellipsis terminator; corresponding parameters shall have compatible types. If one type has a parameter type list and the other type is specified by a function declarator that is not part of a function definition and that contains an empty identifier list, the parameter list shall not have an ellipsis terminator and the type of each parameter shall be compatible with the type that results from the application of the default argument promotions. 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, and the type of each prototype parameter shall be compatible with the type that results from the application of the default argument promotions to the type of the corresponding identifier. (In the determination of type compatibility and of a composite type, each parameter declared with function or array type is taken as having the adjusted type and each parameter declared with qualified type is taken as having the unqualified version of its declared type.)

Wrap-up

tl;dr   Types are important, make use of them

A prototype defines how many arguments a function expects and the type of each argument (again module variadic arguments). This enables a compiler to check if the arguments of a called function are compatible or not. By that a lot of errors/bugs get visible at compile time. Hence the takeaway message is: Every declarator should contain a prototype.

The standard is even pretty clear about the use of a declarator without a prototype:

C11 § 6.11.6 Function declarators

The use of function declarators with empty parentheses (not prototype-format parameter type declarators) is an obsolescent feature.

To make sure that a prototype is always included, GCC has an option -Wstrict-prototypes such that a warning is raised if a prototype is missing (the option is neither implied by -Wall nor by -Wextra).