K&R C Default Argument Promotion Rules
In early K&R C implementations, before the introduction of
function prototypes by the ANSI C standard, the compiler could not
verify whether the arguments supplied at a function call matched the
parameters expected by the function definition. To prevent mismatched
calling conventions and simplify code generation, the compiler applied
"default argument promotions" to every argument passed to a function.
This process automatically widened smaller integer types to
int and single-precision floating-point types to
double, establishing a predictable standard for how data
traveled across the call stack.
The Absence of Function Prototypes
In the original definition of C described by Brian Kernighan and Dennis Ritchie (K&R C), function declarations only specified the return type, omitting any parameter list:
int calculate(); /* Declaration specifies return type, but no parameters */Because separate compilation units could call calculate
without knowing the exact types of its formal parameters, the compiler
needed a universal agreement between the caller and the callee. Without
such an agreement, pushing a 1-byte char where a 2-byte or
4-byte int was expected could corrupt the stack or misalign
CPU registers.
The Standard Promotion Rules
Whenever a function call occurred, the caller applied two primary promotion rules to the arguments:
- Integral Promotions: Any integral argument smaller
than an
int—specificallycharandshort, whether signed or unsigned—was automatically widened to anint. If anunsigned shortcould not fit within a signedinton architectures where they were the same size, it was promoted tounsigned int. - Floating-Point Promotions: Any argument of type
floatwas automatically promoted todouble.
Additionally, array arguments decayed into pointers to their first elements, and function names decayed into function pointers.
Handling Arguments on the Callee Side
In K&R function definitions, parameters were declared using a two-step syntax: the parameter names appeared in the parenthetical list, followed by their type declarations before the opening brace:
int process_data(c, f)
char c;
float f;
{
/* Function body */
}Because of default argument promotions, the caller always passed an
int for c and a double for
f.
To accommodate this, the compiler adjusted the function prologue.
Upon entry, the callee retrieved the widened values (int
and double) from the stack or registers and implicitly cast
them down to the declared local types (char and
float) for use inside the function body.
Architectural Rationale
These promotion rules were chosen to reflect early hardware constraints and optimize compiler simplicity:
- Word Alignment: Early minicomputers, such as the
PDP-11 on which C was developed, handled operations on machine-word
boundaries far more efficiently than sub-word operations. Pushing full
machine words (
int) avoided complex masking and shift operations during stack manipulation. - Floating-Point Hardware: Many early floating-point units executed computations using double-precision registers by default, making single-precision argument passing an unnecessary source of conversion overhead at the call site.
- Variadic Functions: Functions that accept variable
numbers of arguments, such as
printf, relied entirely on these fixed promotions to parse trailing arguments safely using macros likeva_arg.
Legacy in Modern C
When ANSI C (C89) introduced function prototypes, default argument
promotions were made conditional. For prototyped functions, arguments
are converted directly to the types declared by the parameter list.
However, default argument promotions remain active in standard C today
for two specific use cases: when calling functions that lack a
prototype, and when passing arguments to the variable-argument portion
(...) of a variadic function.