Search code examples
cgccinlineinline-functions

A bizzare phenomenon on inline function?


While I tried to understand inline functions, I found myself in a rabbit hole which is very deep. I get to know that inline is a function specifier, that inline is a mere hint to a compiler, that it's up to a compiler whether it perform an inline expansion or not, that a complier needs one definition for inline and another definition for a normal function call, etc.

By the way, it seems I'm not the only one who is having a hard time trying to understand this topic, considering the number of questions on it here. Even though I've tried to read those answers, I still feel cryptic.

Nevertheless, what I'm sure is that there are two correct ways to use inline functions, which are static inline and extern inline:

/* Usage of static inline */
/* This is a file named test.c */
#include <stdio.h>

static inline int fn(void);

int main(void) {
   printf("%d\n", fn());
   return 0;
}

// Does this provide two definitions? I'm not sure.
static inline int fn(void) {
   return 123;
}

/* Output */
$ gcc -o test test.c
$ ./test
123
/* Usage of extern inline */
/* In a file named inline.h */
inline int fn(void) {
   return 321;
}

/* In a file named inline.c */
#include "inline.h"

// So this "declaration" is treated as "definition"?
extern int fn(void);

/* In a file named test.c */
#include <stdio.h>
#include "inline.h"

int main(void) {
   printf("%d\n", fn());
   return 0;
}

/* Output */
$ gcc -o test test.c inline.c
$ ./test
321

Meanwhile, I gave it a try the below code and I got a bizarre result, which is the topic of this question.

/* In a file named test_1.c */
#include <stdio.h>

inline int fn(int a, int b) {
   return a + b;
}
extern int fn(int a, int b);

int wrapper(void);

int main(void) {
   printf("%d\n", wrapper());
   return 0;
}

/* In a file named test_2.c */
inline int fn(int a) {
   return a;
}

int wrapper(void) {
   return fn(123);
}

I wrote two versions of the fn function and I purposely made them different. (fn in test_2.c has only one parameter while fn in test_1.c has two parameters.)

After I compiled this program and executed it, it printed a random number every time I ran it.

$ gcc -o test test_1.c test_2.c
$ ./test
-1466335670
$ ./test
81565994

As far as I guess, the fn(123) call in test_2.c used the definition of fn in test_1.c since the compiler needed a normal definition of fn because it didn't choose to inline it, and fn has an external linkage. (If a function has an external linkage, that function is visible from other files.) Meanwhile, it seems the compiler recognized fn has only one parameter because of the fn definition in test_2.c. However, fn(123) lacked of the second parameter. So I think something went wrong because of this reason. Or perhaps it's just because I wrote a code causing undefined behaviors or something.

I'm sorry to add one more 'I-don't-understand-inline-functions' question, but please give me one more generosity. It would be greatly appreciated if you could explain why this happens. Thank you.

EDIT: This is the environment on which I built the executable. Note: I'm using Windows Subsystem for Linux (WSL).

> wsl -l -v
  NAME      STATE           VERSION
* Ubuntu    Running         2

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.3 LTS
Release:        22.04
Codename:       jammy

$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0

This is the commands I used to build the executable.

$ gcc -o test test_1.c test_2.c

If possible, I would prefer C99, since the book from which I learned C explains C89 & C99. Thank you very much.


Solution

  • I think your code has undefined behavior. C23 draft N3220 6.2.7p2:

    All declarations that refer to the same object or function shall have compatible type; otherwise, the behavior is undefined.

    You have two declarations of fn with incompatible types (different numbers of arguments).

    A possible cause of the actual behavior you observed is that, as you suggested, the compiler chose not to inline the call to fn() in test_2.c (this is normal behavior for gcc when not optimizing: https://godbolt.org/z/dTTGbv9x9), and so emitted a call to the external definition from test_1.c. Since the test_1.c version expected two arguments, but only one was passed, the other probably came from whatever happened to be in the relevant register or memory location, whose value was unpredictable and may have changed from run to run. (If it happened to be an address, and your system uses ASLR, then it might be "random" in a more literal sense.) Obviously, to be certain of this, you'd have to inspect and/or trace the machine code actually generated by your compiler on your system.

    If the declarations had been compatible (say, both taking one int argument), and the calls updated to match, then there should be no UB, but it's still "unspecified" (6.7.5p7) whether the inline or the external definition is the one that's called in each instance. As such, as a matter of reasonable programming practice, you should normally ensure that all definitions are identical (say, by having the definition in a single header file which is #included into every translation unit where it's needed); or at least, that they have functionally identical behavior.

    If you want to have multiple, substantially different definitions (still with compatible declarations), and "roll the dice" on every call, the standard allows that, but it seems much more likely to be a source of bugs than a useful feature.

    (You mentioned being interested in C99: everything in this post is the same for C99 as for C23. The relevant rules have not changed between the two versions.)