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 paragraph 14

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 using the options -Wall -Wextra -pedantic -std=c11 no warning or error is raised, whereas, 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 LLVM 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:

1
2
3
4
5
6
7
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 three is fine, i.e., at this point in time the best the compiler can do is to assume that the call is correct. At line five 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 seven 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 paragraph 7

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 § 6.5.2.2 paragraph 6:

C11 § 6.5.2.2 paragraph 6

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 paragraph 15

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

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).