42

Recently I dug a little bit into old graphics libraries and found libxmi. The site was last updated on 08/09/2000. And in the source code I found the following style of function definitions which I have never seen before:

#include <stdio.h>

int add(a, b) int a, b; { return a + b; }

int main() { printf("%d\n", add(5, 6)); }

Believe it or not but you can compile this with a recent gcc and MSVC (in C mode). When was this type of definitions invented? Is it even standardized ISO C? What is this?

dave
  • 35,301
  • 3
  • 80
  • 160
hefe
  • 661
  • 4
  • 7
  • 42
  • 47
    Yet again, I feel old now... – Jon Custer Feb 01 '22 at 21:08
  • 34
    That's what C was like before ISO standardization. – Fred Larson Feb 01 '22 at 21:08
  • 11
    Without the type declaration, int would haver been assumed, so that was redundant. – Erik Eidt Feb 01 '22 at 21:10
  • 14
    The key sentence in the Wikipedia link above is "Even after the publication of the 1989 ANSI standard, for many years K&R C was still considered the "lowest common denominator" to which C programmers restricted themselves when maximum portability was desired" - an old graphics library would want that maximum portability. – Jon Custer Feb 01 '22 at 21:30
  • 5
    I still use this for code golfing all the time as everything is implicitly int and I can save many bytes. – ErikF Feb 01 '22 at 22:05
  • 4
    fun fact, turbo C (back in 1995) would complain about it being obsolete syntax (as a warning and would compile if you managed to not also screw up other parts of C syntax) if you named the file foo.c; if you named it foo.cpp, it would reject it outright (which was super fun when my professor's examples were in the K&R syntax and our book had the "newer" ANSI standard and no one explained which to use) – Foon Feb 01 '22 at 22:43
  • 8
    @Foon - that syntax was never legal in C++, and it seems that Turbo C (like many systems) would infer the target language from the file extension. – dave Feb 01 '22 at 23:13
  • 2
    Big upvote for this question because I’ve never used this syntax and have barely even seen it and it’s probably the first thing that’s made me feel like not a complete fossil in 20 years. – Frog Feb 02 '22 at 06:33
  • 7
    As if void (*signal(int, void (*)(int)))(int) were any better… – user3840170 Feb 02 '22 at 08:20
  • 3
    When I first learned C, it was the only style of function definition and it was a pain in the arse. – JeremyP Feb 02 '22 at 08:47
  • 4
    After reading the code (twice) and thinking "looks fine to me" I realised it's 37 years since I first learned to write C programs. – Laconic Droid Feb 03 '22 at 13:47
  • I think that goes for me too. Ultrix-32 in the mid-1980s. – dave Feb 03 '22 at 22:18
  • When I first learned C in the late 1990's, the books still gave examples of the K&R argument syntax, but recommended using the "new" ANSI style instead. – dan04 Feb 04 '22 at 01:02
  • 1
    I first learned it this way from a book. I remember thinking, "Why don't we just declare it all inside the parentheses?" – gbarry Feb 05 '22 at 05:28
  • By the mid-80s (well before the first ISO standard), modern parameter specification was becoming ubiquitous (though optional) in compilers. I think I can remember using the modern syntax on Aztec C, generating Z-80 code for CP/M in about 1985-ish. – Flydog57 Feb 08 '22 at 17:40

4 Answers4

73

It's the original C syntax as designed by Dennis Ritchie, so bite your tongue :-)

As mentioned in another answer, the style was not unusual at the time. For an example of early art: FORTRAN II (1958) did not have variable declarations at all. A variable existed by being mentioned. The type of an identifier was determined by its initial letter (I to N INTEGER, otherwise REAL). There was a DIMENSION statement for specification of array identifiers.

Subroutine declarations would therefore have no type information in the SUBROUTINE statement, though may be followed by a DIMENSION.

      SUBROUTINE FOO(VALUES, KOUNT)
      DIMENSION VALUES(KOUNT)

VALUES is a REAL array of KOUNT elements. (We all learned the art of bad spelling)

FORTRAN IV (~1962) added type declarations, but they were optional in general. The implicit declaration and "initial letter" rules still applied. The dummy arguments of subroutines could have type specifiers in subsequent statements, but were not required to.

      SUBROUTINE BAR(VALUES, KOUNT)
      DOUBLE PRECISION VALUES
      DIMENSION VALUES(KOUNT)

In Algol 60, the argument types are specified after the procedure heading, and before the body of the procedure - here just a single statement. Assignment to the procedure name was how a return value was indicated.

integer procedure square(n);  
    value n; integer n;  
    square := n × n;  

By Algol 68, you might find the formal-parameter style a little more familiar. The returned value is the value of the last expression to be executed. The type of 'square' is read as something like 'proc of int returning int'.

proc square = (int n) int :  
    (n × n);

In short, there was nothing unusual at the time about that aspect of C. The style that C initially adopted for function definitions was quite common at the time, so it's not so strange a choice for Ritchie to have made. The style you are more familiar with did not fully emerge in C until ANSI C, in 1989 - 17 years after C was created.

Note too that there was not necessarily a way to state all of the attributes of a formal parameter in a single statement: Algol needed integer for type, and (optionally) value for mechanism.

Incidentally, the reason for the ANSI C change was not so much in the definition of a function -- both forms of function definitions convey the argument types -- but in the declaration of functions, for example in header files. The latter carried no argument information at all - for example all you could say about an external function in K&R style was what it returned: double sin().

This is error-prone; you don't find out (with the compiler technology of the day) until execution time that you've got a mismatch.

While ANSI C added the new syntax, they did not outlaw the old K&R syntax, which remains valid, in the successive ISO standards as well as the original ANSI.

Link: paper from Ritchie on the history of C

dave
  • 35,301
  • 3
  • 80
  • 160
  • 6
    Even the most modern Fortran (2018) still uses this style, so it's not just a thing of the 60's. – TooTea Feb 02 '22 at 15:37
  • 9
    The most modern Fortran is a thing of the 60s being beat into standing up. – Kaz Feb 02 '22 at 20:14
  • 4
    This answer could be read to indicate that K&R style does not conform to ISO C, but to answer "Is it even standardized ISO C?": yes, it is. Every revision of ISO C, up to and including the current one (C17), includes specifications for K&R function definition style alongside its specifications for ANSI definition style. – John Bollinger Feb 03 '22 at 03:17
  • @JohnBollinger - thanks, sentence added to that effect. – dave Feb 03 '22 at 22:15
  • 1
    Does KOUNT have to be spelled wrong to avoid conflict with the built-in COUNT function, or is it just a way to avoid declaring the variable as an integer by exploiting the implicit leading I-N rule? – dan04 Feb 04 '22 at 01:10
  • 1
    I was exploiting the I-N rule (especially in FORTRAN II, which could not express INTEGER COUNT). My knowledge of FORTRAN is too rusty to know whether variables and intrinsics are in separate namespaces. In any case, COUNT was not an intrinsic in FORTRAN II or IV. – dave Feb 04 '22 at 01:24
  • 8
    And so, as they say, GOD is REAL - unless declared INTEGER... – Zeus Feb 04 '22 at 05:53
  • 1
    @dan04 Not sure about ancient Fortran, but anything modern-ish (Fortran 90+) doesn't care. Fortran doesn't even have the concept of "reserved keywords" like other languages, you're explicitly allowed (section 2.5.2 of Fortran 2018, for example) to have a variable named if, end, or integer. – TooTea Feb 04 '22 at 14:34
  • 1
    @TooTea - but dan04's question, as I understood it, was really "can a function and a variable have the same name?". I don't recall the language well enough to know. – dave Feb 04 '22 at 16:00
  • 1
    @another-dave Fair enough, I generalized it a bit too much. I was trying to say that Fortran doesn't have any limitations as to how you can name your identifiers wrt clashing with something defined by Fortran. So to answer the question as asked, yes, a function and a variable (or any two things, really) can have the same name as long as they aren't defined in the same scope. So your local variable will gladly shadow a "global" (host-associated) variable/function/type or one pulled in from elsewhere (intrinsic, use-associated). – TooTea Feb 04 '22 at 16:26
  • 1
    @Zeus: Another variant of that saying I've seen is "But Jesus is an integer." – dan04 Feb 07 '22 at 23:44
  • @another-dave, in the paper you link, Ritchie cites Algol 68 as having influenced C's type structure, so it's interesting that the function definition syntax of K&R C is closer to Algol 60 (parameter types declared after function head/declarator) than Algol 68 (parameter types declared in formal parameter list). Would you know why? Was the Algol 60 syntax easier to parse? – user51462 Dec 02 '22 at 10:25
30

As others have explained, it's the original K&R C style. But why is it this way?

C is the successor to B (Wikipedia). It was typeless, so there was no need to specify which type a function argument had:

/* The following function will print a non-negative number, n, to
   the base b, where 2<=b<=10.  This routine uses the fact that
   in the ASCII character set, the digits 0 to 9 have sequential
   code values.  */

printn(n, b) { extrn putchar; auto a; /* Wikipedia note: the auto keyword declares a variable with automatic storage (lifetime is function scope), not "automatic typing" as in C++11. */

    if (a = n / b)        /* assignment, not test for equality */
            printn(a, b); /* recursive */
    putchar(n % b + '0');

}

(The original source of this example is from the User's Reference to B by Ken Thompson.)

So when Dennis and Ken evolved the language and added types, they needed a way to specify which type an argument had. I don't know the exact reasons for adding this info between ) and { but it does make sense.

A point I remember but don't have a reference to right now: The types default to int if left out, and this was important and intentional. Due to this quirk, some B code can be compiled with C with no or minor changes. This eased porting existing code.

DarkDust
  • 1,488
  • 14
  • 20
13

It’s an old style where type declarations were separate from the naming (and positioning) of the arguments. The style dates from at least the 1960s in languages like FORTRAN, Algol 60, and PL/I.

hs takeuchi
  • 171
  • 3
7

As others have mentioned, this was the style of function declarations before ANSI C. It’s often called K&R C, after Brian Kernighan and Dennis Ritchie. Back in the early ’90s, some books about the then-recent changes to the language complained that the new style of declaring functions was more like C++ than C.

Why was that chosen originally? The calling convention on the computers for which C was originally developed was to pass arguments on the stack. That syntax made it simple to call a function like printf() with any number of arguments of arbitrary type, and have the called function figure out how to interpret them based on the format string. (It was less simple to write the variadic function in K&R C, but that would have been done in assembler back then.) Dennis Ritchie wanted C to be more strongly-typed than B, but not too strictly to write a generic I/O library.

ANSI C provides more complex ways to switch back to the older syntax, to be able to link to existing libraries, or partly back to it for variadic functions. This lets printf() and scanf() be supported, but enables type-checking of function arguments wherever possible. It would later get a simple form of generic function for use in the math library, and later still have that mechanism become available as a building block. Originally, though, there was only one kind of function, and it supported arbitrary arguments.

Davislor
  • 8,686
  • 1
  • 28
  • 34
  • 4
    I think this slightly misses the point of the question. Having int foo(x); int x; does not make variadic functions any easier than int foo(int x). What allows variadic functions is making the provision of any formal argument list (whether as x or int x) optional -- which is possible in both declaration conventions, or at least was until foo(void) was invented. – dave Feb 02 '22 at 15:52
  • 1
    @another-dave The strict prototyping actually does preclude variadic functions, by forcing the caller to pass only that number of arguments with those types. This is why C89 needed to add the more complex ,(const char* format, ...) syntax to enable it. But, before ANSI C, that was how all C functions worked. – Davislor Feb 02 '22 at 16:36
  • Of course. As I said, it's not where the 'int' goes that makes the difference. – dave Feb 02 '22 at 17:08
  • @another-dave The context in which it does is, if the prototype specifies that the function takes one parameter of type int, it statically type-checks every call to the function. If there is no prototype, the compiler still allows the function to be called with any number and type of arguments. (It also changes how the argument types are implicitly converted.) If there’s no prototype preceding the call, the compiler has no way of knowing what the K&R-style type signature in the other module says. – Davislor Feb 03 '22 at 01:54
  • Yes, that's why foo(void) needed to be invented. – dave Feb 03 '22 at 01:57
  • @another-dave Yep! – Davislor Feb 03 '22 at 01:57
  • 3
    @another-dave: On most systems, a function of the form int foo(x,y,z) int x,y,z; {...} could safely be invoked with 1, 2, or 3 arguments if it never made use of any arguments that weren't passed to it. One could, for example, have a function int sendPacket(dat, length) char *dat; int length;, that would send an empty packet, ignoring the length argument, if dat was null. Call-side code which passed null without specifying a length could then be smaller than would be possible if both arguments had to be passed. Not all platforms could efficiently support support semantics, however. – supercat Feb 03 '22 at 17:04
  • Unfortunately, K&R's int foo() semantics severely constrain C's default calling convention. You have to pass arguments in-memory just in case someone ever passes more arguments than the platform has registers for passing arguments, and you have to make the call site responsible for stack cleanup (because the callee doesn't know how many bytes you pushed onto the stack). Hence API's like Win32 that use non-default calling conventions (__pascal, __stdcall, __fastcall, or whatever) for the sake of efficiency. – dan04 Feb 04 '22 at 01:21
  • I suspect not entirely true for all machines. For example, on VAX, the stack frame and/or the argument list constructed by CALLS knew the actual argument count, and the RET instruction itself could correctly restore the stack (I forget whether the count came into it, or it was just a matter of restoring the previous frame pointer). But that required a sufficiently rich register set to support frame pointer, stack pointer, argument pointer as separate entities. – dave Feb 04 '22 at 02:43