1

I just resolved an absolute headbanger of a problem, and the issue was so simple, yet so elusive. So frustratingly hidden behind a lack of compiler feedback and an excess of compiler complacency (which is rare!). During writing this post, I found a few similar questions, but none that quite match my scenario.

I have a function declaration with no args, a call to that function with no args, and the function definition below with args. Somehow, C manages to successfully call the function, no warning, no error, but very undefined behaviour. Where does the function get the missing argument from? Why don't I get a linker error since the no-arg function isn't defined? Why don't I get a compiler error because I'm redefining a function with a different signature? Why, oh why, is this allowed?

Compiling as C++ code (gcc -x c++, enabling Compile To Binary on Godbolt) I get a linker error as expected, because of course C++ allows overloading, and the no-arg overload isn't defined. By checking with Godbolt, compiling with Clang and MSVC as C code also both build successfully, with only MSVC spitting out a minor warning.

Here is my reduced example for Godbolt.

// Compile with GCC or Clang -x c -Wall -Wextra
// Compile with MSVC /Wall /W4 /Tc

#include <stdio.h>
#include <stdlib.h>

// This is just so Godbolt can do an MSVC build
#ifndef _MSC_VER
#  include <unistd.h>
#else
#  define read(file, output, count) (InputBuffer[count] = count, fd)
#endif

static char InputBuffer[16];

int ReadInput(); // <-- declared with no args

int main(void)
{
    int count;
    count = ReadInput(); // <-- called with no args

    printf("%c", InputBuffer[0]); // just so the results, and hence the entire function call,
    printf("%d", count);          // don't get optimised away by not being used (even though I'm 
    return 0;                     // not using any optimisation... just being cautious)
};

int ReadInput(int fd) // <-- defined with args!
{
    return read(fd, InputBuffer, 1); // arg is definitely used, it's not like it's optimised away!
};
Ashley Miller
  • 836
  • 6
  • 8
  • 1
    You can find an explanation at : https://en.cppreference.com/w/c/language/function_declaration . This explain that declaring function with no parameter is not an error. The usage with no parameter may be undefined behaviour. – Ptit Xav Mar 19 '21 at 13:41
  • "The declarator `f()` is a declarator that declares a function that takes unspecified number of parameters" definitely clears up the declaration issue. I don't think I'm satisfied that later offering a definition, and not calling pursuant to that definition, is "undefined" (but still completely legal). – Ashley Miller Mar 19 '21 at 13:54
  • 1
    Keep in mind the "one pass" nature of C. When the compiler sees `ReadInput();` it has so far only seen the declaration `int ReadInput();` which doesn't specify type or number of parameters, so there's no conflict. And by the time it gets to the definition `int ReadInput(int fd)`, it has forgotten about anything that occurred inside previous functions, so it doesn't know there was a problem. All it remembers is the declaration `int ReadInput();` and that's also consistent with the definition, so everything seems okay at compile time. [...] – Nate Eldredge Mar 19 '21 at 23:02
  • 1
    But it's UB at runtime since `ReadInput(int)` is expecting an argument that wasn't actually passed. (A compiler could be smarter, of course, but the C standard wants to allow "one pass, one function at a time" compilers like this, so it can't require a compiler error or diagnostic on this code.) – Nate Eldredge Mar 19 '21 at 23:02

3 Answers3

2

Where does the function get the missing argument from?

Typically, the called function is compiled to get its parameters from the places the arguments would be passed according to the ABI (Application Binary Interface) being used. This is necessarily true when the called function is in a separate translation unit (and there is no link-time optimization), so the compiler cannot adjust it according to the calling code. If the call and the called function are in the same translation unit, the compiler could do other things.

For example, if the ABI says the first int class parameter is passed in processor register r4, then the called function will get its parameter from register r4. Since the caller has not put an argument there, the called function gets whatever value happens to be in r4 from previous use.

Why don't I get a linker error since the no-arg function isn't defined?

C implementations generally resolve identifiers by name only. Type information is not part of the name or part of resolution. A function declared as int ReadInput() has the same name as a function declared as int ReadInput(int fd), and, as far as the linker is concerned, a definition of one will satisfy a reference to the other.

Why don't I get a compiler error because I'm redefining a function with a different signature?

The definitions are compatible. In C, the declaration int ReadInput() does not mean the function has no parameters. It means “There is a function named ReadInput that returns int, and I am not telling you what its parameters are.

The declaration int ReadInput(int fd) means “There is a function named ReadInput that returns int, and it takes one parameter, an int. These declarations are compatible; neither says anything inconsistent with the other.

Why, oh why, is this allowed?

History. Originally, C did not supply parameter information in function declarations, just in definitions. The prototype-less declarations are still allowed so that old software continues to work.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • "C implementations generally resolve identifiers by name only." I had to look this up further, because I've definitely seen mangled C names that include the length of the arglist (`_ReadInput@4`). Looks like that's not what happens for the default `cdecl` calling convention. How annoying. Another reason why this can slip through the cracks. – Ashley Miller Mar 19 '21 at 14:01
2

Other answers explained why it is legal to call a function that was declared without a prototype (but that it is your responsibility to get the arguments right). But you might be interested in the -Wstrict-prototypes warning option accepted by both GCC and clang, which is documented to "Warn if a function is declared or defined without specifying the argument types." Your code then yields warning: function declaration isn't a prototype.

Try it on godbolt.

(I'm kind of surprised this warning isn't enabled with -Wall -Wextra.)

Nate Eldredge
  • 48,811
  • 6
  • 54
  • 82
  • *I'm kind of surprised this warning isn't enabled with -Wall -Wextra* one would think so, yes. – anastaciu Mar 19 '21 at 22:55
  • I agree. Thanks to all the answers and comments, I get that historically this was just the way it worked, and that this can still be a deliberate programming choice now. But I still feel it's easy and dangerous enough that it should be a standard warning. Just to ask, "are you sure? You understand the implications, right?" (To which I would have previously answered, "No... what implications? Thanks, compiler warnings! You're great!") – Ashley Miller Mar 21 '21 at 06:07
1

In C, unlike in C++, declaring a function with no arguments means that the function may have as many arguments as you'd like. If you want to make it really not have any arguments, you just have to explicitly declare that:

int ReadInput(void);
anastaciu
  • 23,467
  • 7
  • 28
  • 53
  • 1
    Oh fantastic! Compiler errors aplenty! I'm almost embarrassed that I didn't do this. I always try to use `(void)` in classic C; not so bothered in C++. I was under the impression we use `(void)` because `()` is illegal, from the old days. But after more experimentation, it's still legal under `--std=c89`. – Ashley Miller Mar 19 '21 at 13:48
  • 1
    @ashley_c4, yes it does, in fact it's a legacy issue, anyway, no embarassment needed, I don't do it either, just so happens I know this for some reason, it could have been the other way around ;) – anastaciu Mar 19 '21 at 13:52