0

I'd like information on the behavior of pre-standard "K&R-style" function declaration syntax when used in conjunction with explicit function protoypes as introduced by ANSI. Specifically, the syntax that looks like this:

int foo(a)
    int a;
{
    /* ... */
}

as opposed to like this:

int foo(int a) {
    /* ... */
}

Note that I am referring specifically to the function declaration syntax, not the usage of unprototyped functions.

Much has been made of how the former syntax does not create a function prototype. My research indicates that, if the function were defined as above, a subsequent call foo(8, 6, 7, 5, 3, 0, 9) would result in undefined behavior; whereas with the latter syntax, foo(8, 6, 7, 5, 3, 0, 9) would actually be invalid. This makes sense, but I explicitly forward-declare all my functions in the first place. If the compiler ever had to rely on a prototype generated from the definition, I'd already consider that a flaw in my code; so I make sure to use compiler warnings that notify me if I ever fail to forward-declare a function.

Assuming that proper forward-declarations are in place (in this case, int foo(int);), is the K&R function declaration syntax still unsafe? If so, how? Does the usage of the new syntax negate the prototype that's already there? At least one person has apparently claimed that forward-declaring functions before defining them in the K&R style is actually illegal, but I've done it and it compiles and runs just fine.

Consider the following code:

/*  1 */ #include <stdio.h>
/*  2 */ void f(int); /*** <- PROTOTYPE IS RIGHT HERE ****/
/*  3 */ void f(a)
/*  4 */     int a;
/*  5 */ {
/*  6 */     printf("YOUR LUCKY NUMBER IS %d\n", a);
/*  7 */ }
/*  8 */ 
/*  9 */ int main(argc, argv)
/* 10 */ int argc;
/* 11 */ char **argv;
/* 12 */ {
/* 13 */    f(1);
/* 14 */    return 0;
/* 15 */ }

When given this code verbatim, gcc -Wall and clang -Weverything both issue no warning and produce programs that, when run, print YOUR LUCKY NUMBER IS 1 followed by a newline.

If f(1) in main() is replaced with f(1, 2), gcc issues a "too many arguments" error on that line, with the "declared here" note notably indicating line 3, not line 2. In clang, this is a warning, not an error, and no note indicating a declaration line is included.

If f(1) in main() is replaced with f("hello world"), gcc issues an integer conversion warning on that line, with a note indicating line 3 and reading "expected 'int' but argument is of type 'char *'". clang gives a similar error, sans note.

If f(1) in main() is replaced with f("hello", "world"), the above results are both given, in sequence.

My question is this: assuming function prototypes are already provided, is the K&R syntax any less safe than the style with inline type keywords? The answer indicated by my research is, "Nope, not a bit", but the overwhelmingly negative, apparently near-unanimous opinion of the older style of type declaration makes me wonder if there isn't something I'm overlooking. Is there anything I'm overlooking?

Foobie Bletch
  • 214
  • 1
  • 9
  • 5
    Can't stop wondering, why is that even a question? Do you feel compelled to write K&R C nowadays? – SergeyA Apr 12 '19 at 19:18
  • 2
    That you need to go for extra steps to make it "safe" is good enough reason to avoid it. It's been deprecated, so it could be *removed* from future C standards. The primary aim to deprecate it was to improve type-safety of the language. So it makes sense to not use it. – P.P Apr 12 '19 at 19:42
  • @SergeyA: I like the old syntax better. I could go into why, but my reasons for liking it better aren't relevant to the question. – Foobie Bletch Apr 12 '19 at 20:13
  • @usr: That's not the question I'm asking. I'm not asking "why shouldn't I use this thing?" I'm asking "Assuming I adhere to certain best practices that I should be adhering to anyway, is it unsafe to use this thing, and if so, specifically why not?" – Foobie Bletch Apr 12 '19 at 20:17
  • 4
    @FoobieBletch suit yourself, but be mindful that once you finish your college and start writing programs for money, you might quickly find out that your love to retro-syntax is not shared. – SergeyA Apr 12 '19 at 20:22
  • Actually, you have better diagnostic with the standard notation using clang: [a warning](https://wandbox.org/permlink/LRofc7X6Cq4q7LrU) vs. [an error](https://wandbox.org/permlink/OiBrBkGdr8OTjjaA). That seems safer to me. – Bob__ Apr 12 '19 at 20:39
  • 2
    I think this is a dupe of this question: https://stackoverflow.com/questions/1255775/default-argument-promotions-in-c-function-calls That question itself explains why using prototypes with K&R style functions is a **very bad idea**. I'm not dupe-hammering this yet, but I'm tempted. – Andrew Henle Apr 12 '19 at 21:02
  • [This answer](https://stackoverflow.com/a/1255818/4756299) explains the consequences of using prototypes with K&R style functions, answering the question. It's a dupe. – Andrew Henle Apr 12 '19 at 21:20
  • @AndrewHenle: That answer does not discuss the semantics of when both a prototype and a non-prototype declaration are visible, which this question asks about. It discusses using one in one translation unit and the other in another, which is different. – Eric Postpischil Apr 12 '19 at 21:59

5 Answers5

2

My research indicates that, if the function were defined as above, a subsequent call foo(8, 6, 7, 5, 3, 0, 9) would result in undefined behavior; whereas with the latter syntax, foo(8, 6, 7, 5, 3, 0, 9) would actually be invalid.

This is correct. Given int foo(a) int a; {}, the call has undefined behavior per C 2018 6.5.2.2 6:

If the expression that denotes the called function has a type that does not include a prototype,… If the number of arguments does not equal the number of parameters, the behavior is undefined.

And, given int foo(int a);, the call violates a constraint, per C 2018 6.5.2.2 2:

If the expression that denotes the called function has a type that includes a prototype, the number of arguments shall agree with the number of parameters.

Assuming that proper forward-declarations are in place (in this case, int foo(int);), is the K&R function declaration syntax still unsafe?

If a function has both a declaration with a prototype, as it would in your forward declaration, and a definition without the prototype (using the old K&R syntax), the resulting type of the identifier is that of the prototyped version. The type of a function declared with a parameter list can be merged with the type of a function declared with the K&R syntax. First C 2018 6.7.6.3 15 tells us the two types are compatible:

For two function types to be compatible, both shall specify compatible return types. … 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.…

Then C 2018 6.2.7 3 tells us they can be merged:

A composite type can be constructed from two types that are compatible; it is a type that is compatible with both of the two types and satisfies the following conditions:

— If only one type is a function type with a parameter type list (a function prototype), the composite type is a function prototype with the parameter type list.

And C 2018 6.2.7 4 tells us the identifier takes on the composite type:

For an identifier with internal or external linkage declared in a scope in which a prior declaration of that identifier is visible, if the prior declaration specifies internal or external linkage, the type of the identifier at the later declaration becomes the composite type.

Thus, if you have both int foo(int a); and int foo() int a; {}, foo has type int foo(int a).

This implies that if every function is declared with a prototype, defining them without a prototype is just as safe as defining them with a prototype, in regard to the semantics of calling them. (I do not comment with regard to the possibility that style or another might be more or less susceptible to errors caused by mistaken edits or other aspects unrelated to actual semantics of function calls).

Note however, that the types in the prototype must match the types in the K&R-style definition after default argument promotion. For example, these types are compatible:

void foo(int a);
void foo(a)
char a; // Promotion changes char to int.
{
}

void bar(double a);
void bar(a)
float a; // Promotion changes float to double.
{
}

and these types are not:

void foo(char a);
void foo(a)
char a; // char is promoted to int, which is not compatible with char.
{
}

void bar(float a);
void bar(a)
float a; // float is promoted to double, which is not compatible with float.
{
}
Community
  • 1
  • 1
Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
1

The code in the question isn't very problematic; handling int presents few problems. Where it gets tricky is in function like this one:

int another(int c, int s, double f);

int another(c, s, f)
    short s;
    float f;
    char c;
{
    return f * (s + c);  // Nonsense - but it compiles cleanly enough
}

Note that the prototype for that is not

int another(char c, short s, float f);

It is interesting that GCC accepts both prototypes unless you add -pedantic (or -Wpedantic) to the compilation options. This is a documented GCC extension — §6.38 Prototypes and Old-Style Function Definitions. By contrast, clang complains (as a warning if the -Werror option isn't specified — and it complains by default, even without -Wall or -Wextra, etc.):

$ clang -O3 -g -std=c11 -Wall -Wextra -Werror -Wmissing-prototypes -Wstrict-prototypes -c kr19.c
kr19.c:7:10: error: promoted type 'int' of K&R function parameter is not compatible with the
      parameter type 'char' declared in a previous prototype [-Werror,-Wknr-promoted-parameter]
    char c;
         ^
kr19.c:2:18: note: previous declaration is here
int another(char c, short s, float f);
                 ^
kr19.c:5:11: error: promoted type 'int' of K&R function parameter is not compatible with the
      parameter type 'short' declared in a previous prototype [-Werror,-Wknr-promoted-parameter]
    short s;
          ^
kr19.c:2:27: note: previous declaration is here
int another(char c, short s, float f);
                          ^
kr19.c:6:11: error: promoted type 'double' of K&R function parameter is not compatible with the
      parameter type 'float' declared in a previous prototype [-Werror,-Wknr-promoted-parameter]
    float f;
          ^
kr19.c:2:36: note: previous declaration is here
int another(char c, short s, float f);
                                   ^
3 errors generated.
$

As long as you recognize this discrepancy for the shorter types and your prototypes match the promoted types, you should not actually run into trouble defining the functions using K&R notation and declaring them using prototype notation.

However, there is no obvious benefit to the discrepancy — if you've written the prototype correctly in a header, why not use that prototype declaration as the basis of the function definition?

I am still working on a code base that has some K&R function definitions from the 80s and early 90s. Most such functions do have a prototype in a header, with the promoted types in the prototypes. I am actively cleaning it up to convert all function definitions to prototype notation, ensuring that there is always a prototype in scope for functions before (non-static) functions are defined or any function is called. The local convention is to redundantly declare static functions at the top of the file; that works too.

In my own code, I never use K&R notation — not even for parameterless functions (I always use function(void) for those).

Since it is explicitly marked as 'obsolescent' in the standard (C11 §6.11 Future directions, I recommend strongly against using the K&R notation in any modern C code:

  • §6.11.6 Function declarators

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

  • §6.11.7 Function definitions

    The use of function definitions with separate parameter identifier and declaration lists (not prototype-format parameter type and identifier declarators) is an obsolescent feature.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
0

There is no difference at all. You can define them as you prefer. They are exchangeable. But of course no one sane would advice to use the prehistoric notation.

My research indicates that, if the function were defined as above, a subsequent call foo(8, 6, 7, 5, 3, 0, 9) would result in undefined behavior; whereas with the latter syntax, foo(8, 6, 7, 5, 3, 0, 9) would actually be invalid.

Your research is wrong. If the language standard allows using functiuons without the prototypes,

https://onlinegdb.com/SkKF4DAtE

int main()
{
    printf("%d\n", foo(2,3,4,5,6,7,8,9));
    printf("%d\n", foo1(2,3,4,5,6,7,8,9));

    return 0;
}

int foo(a)
int a;
{
    return a*a;
}

int foo1(int a)
{
    return a*a;
}
0___________
  • 60,014
  • 4
  • 34
  • 74
  • This is in danger of getting very off-topic, but I can think of plenty of reasons to use the older style. It's more orthogonal with the rest of the language. It's consistent with mathematical idioms in natural language ("Let _foo_ ( _a_ ) for some integer _a_ be defined as follows:"). Subjectively speaking, it's more readable, and especially so if you have to deal with lengthy names for qualified types, function pointers, function pointers that return qualified types and have qualified types in their parameter lists, etc. – Foobie Bletch Apr 12 '19 at 19:31
  • @FoobieBletch You can use the new style for both the prototype, and the definition, and still put each argument on its own line. For the most part C doesn't care about whitespace, and doesn't treat newlines any differently than spaces. So you have a lot of flexibility in your formatting. – user3386109 Apr 12 '19 at 19:40
  • @user3386109 I know C is whitespace-insensitive, but you're still alternating between keyword and identifier if you put each parameter on its own line. It seems like long argument lists would be much more manageable in the old format rather than the new. – Foobie Bletch Apr 12 '19 at 20:10
  • @FoobieBletch So basically, you're saying that you want `int foo(a, b, c)` in addition to the argument list. That of course can be accomplished with a comment `// int foo(a, b, c)`. Alternating keywords and identifiers unavoidable, it is required by both the old style and the new style. – user3386109 Apr 12 '19 at 20:19
  • 2
    @FoobieBletch About *"It seems like long argument lists would be much more manageable in the old format rather than the new"*, I respectfully disagree. You shouldn't have a long argument list in the first place (use structs), but if it's the case, with the old notation you have to repeat all those parameter in the correct order three times and that is more prone to errors. – Bob__ Apr 12 '19 at 20:33
  • @Bob__: Note that you _can_ (but usually _shouldn't_) define the parameters in any order between the `function(a, b, c)` and the `{` in a K&R function definition. – Jonathan Leffler Apr 12 '19 at 20:53
  • 1
    Re “Your research is wrong”: That is wrong. Calling a function declared with a prototype with the wrong number of parameters is a constraint violation (per OP’s phrasing, “would be invalid”). Calling a function not declared with a prototype with the wrong number of parameters has undefined behavior. (It does not violate a constraint, so the compiler may accept it, and the C standard does not say what the resulting program will do.) – Eric Postpischil Apr 12 '19 at 20:55
  • A corollary of the above is that “There is no difference at all” and “They are exchangeable” are also wrong. But additional differences include the types of the arguments that result, as the default argument promotions are performed for call to a function declared only with K&R style but are not performed for a call to a function declared with a prototype. – Eric Postpischil Apr 12 '19 at 20:56
  • 1
    *There is no difference at all.* Absolutely, utterly incorrect. When you supply a prototype with arguments, those arguments do not undergo default argument promotions. But a function written in K&R style expects its arguments to have undergone argument promotion. That is a huge difference. It only "works" in your example because `int` doesn't undergo promotion. Try it with a `float` or some other arguments that do. – Andrew Henle Apr 12 '19 at 20:57
0

Note that I am referring specifically to the function declaration syntax, not the usage of unprototyped functions.

You seem to be playing a little fast and loose with language. I think you mean you're talking about the effect of the choice of function definition syntax. Function definitions provide function declarations, and those declarations may (ANSI style) or may not (K&R style) include prototypes. Function declarations that are not part of definitions (forward declarations) also may or may not provide prototypes. It is important to understand that behavior differs a bit depending on whether there is an in-scope prototype.

Much has been made of how the former syntax does not create a function prototype. My research indicates that, if the function were defined as above, a subsequent call foo(8, 6, 7, 5, 3, 0, 9) would result in undefined behavior;

Yes. This arises from a language constraint (so not only is behavior undefined, but violations must be diagnosed). In C11, the relevant text is paragraph 6.5.2.2/2:


If the expression that denotes the called function has a type that includes a prototype, the number of arguments shall agree with the number of parameters. Each argument shall have a type such that its value may be assigned to an object with the unqualified version of the type of its corresponding parameter.


Note that that is about agreement of function-call arguments with an in-scope prototype, regardless of whether the prototype actually matches the function's definition. (But other rules require all declarations of the same function type, including its definition, to be compatible.)

whereas with the latter syntax, foo(8, 6, 7, 5, 3, 0, 9) would actually be invalid.

Yes? You seem to be drawing a distinction that I do not follow between what is "invalid" and what has undefined behavior. Code that is "invalid" by any definition I can think of definitely has undefined behavior. Anyway, the behavior in this case too is explicitly undefined, for if there is an in-scope prototype, then 6.5.2.2/2 (above) applies regardless of the form of the function's definition. If there is not an in-scope prototype then paragraph 6.5.2.2/6 (in C11) applies instead. It says, in part:


If the expression that denotes the called function has a type that does not include a prototype [...] If the number of arguments does not equal the number of parameters, the behavior is undefined.


The compiler might not recognize or diagnose such an error, and you might test it on some particular implementation without observing any ill effects, but the behavior is definitely undefined, and under some circumstances, on some implementations, things will break.

My question is this: assuming function prototypes are already provided, is the K&R syntax any less safe than the style with inline type keywords?

If the same prototype for function f() is in scope both where the function is defined and where it is called (good practice in any case), and if the K&R-style definition of f() specifies the same return type and parameter types that the prototype does, then everything will work fine. The multiple compatible declarations in scope at the function definition are combined to form a composite type for the function that will happen to be the same as the type declared by the prototype. The semantics are exactly as if the definition used ANSI syntax directly. This is addressed in section 6.2.7 and related sections of the standard.

There is, however, an additional maintenance burden in maintaining a function definition that does not lexically match its prototype. There is also increased opportunity for errors arising from the parameter types in the parameter declaration list not matching the prototype, and in this sense the approach is less safe than simply using ANSI style throughout.

However, if there is no prototype for f() in scope at the point of its K&R-style definition, and if any of its parameters have type float or integer types smaller than int, then there is more opportunity for error. Notwithstanding the declared parameter types, the function implementation will expect the arguments to have been subjected to the default argument promotions, and the behavior is undefined if in fact they have not been. It is possible to write a prototype that anticipates that need, for use where f() is called, but it is all too easy to get that wrong. In this sense the combination you propose is indeed less safe.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • Where you write “No” in bold, your text appears to agree with OP. They say that calling `foo(8, 6, 7, 5, 3, 0, 9)` when there is a prototype for one argument is invalid, and you say “No” and then say 6.5.2.2 2 applies, which is a constraint violation, meaning the code must get an error message, which is what OP means by invalid. Then, regarding the call when there is not a prototype visible, OP says the behavior is undefined, and you write “the behavior is definitely undefined.” That looks like “Yes” to me, not “No.” – Eric Postpischil Apr 12 '19 at 22:07
  • Thank you, @EricPostpischil. I misread the OP's "invalid" as "valid", I think because they seem to be suggesting that their example is "invalid" but does not have undefined behavior. I have updated the answer. – John Bollinger Apr 12 '19 at 22:16
  • The distinction between "undefined behavior" and "invalid" that I'm drawing is, to my understanding, that undefined behavior is simply behavior that the standard does not specify, but which may compile and run with implementation-dependent results, while invalid code is code that will definitely be rejected by a standards-compliant compiler. – Foobie Bletch Apr 13 '19 at 01:37
  • Then I should switch back to "no", @FoobieBletch. The standard does not require conforming implementations to reject *anything*, not even code with serious, blatant syntax errors. Not even code that violates language constraints. This is the space in which compilers play for implementing extensions. There is certainly a large class of code that I am confident will not be accepted by any C compiler I know, but the standard does not draw any lines in this area. – John Bollinger Apr 13 '19 at 13:18
-2

My question is this: assuming function prototypes are already provided, is the K&R syntax any less safe than the style with inline type keywords?

It is downright WRONG. See Default argument promotions in C function calls for details.

It is also undefined behavior. Per 6.5.2.2 Function calls, paragraph 9 of the C11 standard:

If the function is defined with a type that is not compatible with the type (of the expression) pointed to by the expression that denotes the called function, the behavior is undefined.

You should NEVER mix prototypes with functions defined in the old K&R style.

A function written in K&R style expects that its arguments have undergone default argument promotion when they were passed.

Code that calls a function with a prototype does not promote the arguments to the function.

So the arguments passed to the function are not what the function expects to get.

End of story.

Do not do it.

Andrew Henle
  • 32,625
  • 3
  • 24
  • 56
  • Although I concur that mixing K&R-style function definitions with prototypes is a bad idea -- perhaps bad enough to call if "WRONG" -- it does not necessarily produce undefined behavior. There are other details of this answer that I think are incorrect, too. Eric was swifter than me with an answer that covers all the details, but he and I are in pretty much complete agreement. – John Bollinger Apr 12 '19 at 21:31
  • (a) “It is downright WRONG” is false. The behavior OP asks about (using both prototyped declarations and K&R definitions) is defined by the C standard. (b) That behavior is not undefined. (The question mentions a call that would have undefined behavior, but that is mentioned in discussion; it is not the question.) (c) “You should NEVER mix…” is opinion. The question is tagged language-lawyer, for which it is not unusual to have questions asking about technical interpretations regardless of whether they are good practice or not. – Eric Postpischil Apr 12 '19 at 22:13