Should I place the parameter storage class specifier in the function definition or in both the declaration and definition?

The key provision is that every declaration of the function must specify a compatible type for it. That requires compatible return types, and, for declarations such as yours that contain parameter lists, compatible type for each pair of corresponding parameters.

The question then becomes whether storage-class specifiers differentiate types. They do not, though the standard says that indirectly, by omission of storage-class specifiers from its discussion of type derivation. The property specified by a storage-class specifier in an object's declaration is therefore separate from that object's type.

Moreover, C89 specifically says

The storage-class specifier in the declaration specifiers for a parameter declaration, if present, is ignored unless the declared parameter is one of the members of the parameter type list for a function definition.

(emphasis added). A function definition is a declaration accompanied by a function body, as opposed to a forward declaration, so your two codes have identical semantics.

With and without the register storage class specific declaration, the code compiles correctly (I tried gcc, VC++ and Watcom), I could not find any information in the ISO/ANSI C89 standard on what is the correct way to do, or is it ok if i just put the register keyword in the function definition?

Personally, I would be inclined to make each forward declaration identical to the declaration in the corresponding function definition. This is never wrong if the function definition itself is correct.

HOWEVER,

  1. The register keyword is a relic. Compilers are not obligated to make any attempt at all to actually assign register variables to registers, and modern compilers are a lot better than humans at deciding how to assign variables to registers and otherwise to generate fast code anyway. As long as you're converting old code, I would take the opportunity to remove all appearances of the register keyword.

  2. C89 is obsolete. The latest version of the standard is C 2018; C 2011 is widely deployed; and C99 (also, technically, obsolete) is available almost everywhere. Perhaps there is a good reason for you to target C89, but you should strongly consider instead targeting C11 or C18, or at least C99.


The C89 standard does say this (§ 3.5.4.3 External definitions):

The only storage-class specifier that shall occur in a parameter declaration is register.

So, it would appear that while register is permissible as a function parameter storage class specifier, I still believe that whether or not this is honored really depends on the architecture and calling convention for the function.

Since you mentioned Watcom and C89, I'm going to assume you're targeting x86-16. The typical calling conventions for x86-16 (pascal, stdcall, and cdecl) all require parameters to be pushed on the stack, not in registers, so I doubt that the keyword would actually modify how the parameters are passed to the function at the call site.

Consider, you have the following function definition:

int __stdcall add2(register int x, register int y);

The function goes into the object file as _add2@4 per requirements for stdcall. The @4 indicates how many bytes to remove from the stack at function return. The ret imm16 (return to calling procedure and pop imm16 bytes from stack) instruction is used in this case.

add2 will then have the following ret at the end:

ret 4

If 4 bytes were not pushed on the stack at the call site (i.e. because the parameters were actually in registers), your program now has a misaligned stack and crashes.


Empirically on gcc and clang, the register storage class on function params is behaving the same as top-level qualifiers on params: only the ones in the definition (not a previous prototype) count.

(as for top level qualifiers, they're also discarded when type compatibility is considered, i.e., void f(int); and void f(int const); are compatible prototypes, but storage classes aren't part of types so type compatibility isn't an issue with them in the first place)

From the point of view of a C programmer, the only observable upshot of register in C is that the compiler will not let you take the address of the declared object.

When I do:

void f(int A, int register B);

void f(int register A, int B) 
{
    /*&A;*/ //doesn't compile => A does have register storage here
    &B; //compiles => B doesn't have register storage here;
        //the register from the previous prototype wasn't considered
}

then &B compiles, but &A doesn't, so only the qualifiers in the definition appear to count.

I think that if you do need those register, your best bet is to use it consistently in both places (the register in prototypes could theoretically modify how calls are made).