The main effect of inline
is to allow multiple definitions of the same function (because the definition is typically in a header included in multiple translation units).
The name "inline" is a red herring; like with the register
hint, it has become obsolete with modern compilers, except for the "accept multiple definitions" effect.
Modern compilers choose to inline or not entirely independent of the presence of the inline
keyword.
Given the non-mandatory nature of inline
, it makes sense that a build system tries to treat a function with a missing definition as non-inline (after issuing the mandatory diagnostic message) and tries to resolve the symbol at link time.
The linker error does not occur because the function definition is missing at the call site; as you correctly observed, that is the compiler's domain. Here is a short transcript of two files, one defining a function declared inline, one defining an inline function which we'll change into an ordinary function in a second step, demonstrating that this will work. I compile both and inspect the object files with nm
. nm
inspects object files; we use it to display "global symbols" (exported or unresolved). A "T" indicates that the function is defined in the "text", i.e., code section, as one would expect.
$ cat other.cpp
inline int InlineFun() { return 3; }
int Fun() { return InlineFun(); }
$ gcc -c -O1 other.cpp
$ nm -g --defined-only other.o
0000000000000000 T _Z3Funv
$ cat perhaps-inline.cpp
inline int InlineFun();
int main(){ return InlineFun(); /* From other TU */ }
$ gcc -O1 -c perhaps-inline.cpp
perhaps-inline.cpp:1:12: warning: inline function ‘int InlineFun()’ used but never defined
1 | inline int InlineFun();
| ^~~~~~~~~
$ g++ -O1 other.o perhaps-inline.o -o perhaps-inline
/usr/lib/gcc/x86_64-pc-msys/13.2.0/../../../../x86_64-pc-msys/bin/ld: perhaps-inline.o:perhaps-inline:(.text+0xa): undefined reference to `InlineFun()'
collect2: error: ld returned 1 exit status
The function declared inline is not visible as a global symbol and is not available for linking against. Indeed, inspecting the object code confirms that no code at all is generated for it.
Absent the inline definition, gcc treated the call of InlineFunc()
in main()
as a regular function call, with a warning; the error is simply that no such function was provided. Let's change that. We'll eliminate the inline
in the function definition in other.cpp, making it a regular one:
$ cat other.cpp
int InlineFun() { return 3; }
int Fun() { return InlineFun(); }
$ gcc -c -O1 other.cpp
$ nm -g --defined-only other.o
0000000000000006 T _Z3Funv
0000000000000000 T _Z9InlineFunv
$ g++ -O1 other.o perhaps-inline.o -o perhaps-inline
$ g++ -O1 other.o perhaps-inline.o -o perhaps-inline
$ ./perhaps-inline
$ echo $?
3
Voila: Works.
The linker error in your example occurs indeed because the function is declared inline at the definition site.
The symbol is simply not created. To link successfully, remove the inline
where the function is defined. (We see that gcc does not "decorate" the name of an inline function in any special fashion.)
Let's dive a bit into the standard wording and the behavior of two compilers I have at hand.
The C++ 2020 standard requires in 6.3/11:
A definition of an inline function or variable shall be reachable from the end of every definition domain in which it is odr-used outside of a discarded statement.
There is a bit of jargon here:
- Absent modules, a definition domain is the translation unit.
- odr-used: "Odr" stands for the "one definition rule" that variables etc. may only be defined once in each translation unit. The chapter establishes which uses fall under that category. I don't really understand that opaque language, but I'm sure function calls do.
- The wording reachable is quite interesting: It does not say the translation unit must contain the definition! See the end of the answer for details.
- Discarded statements are parts of templates or const-expressions which can be, well, discarded at compile time.
As we'll see, "reachable" gives the compiler builders some leeway. Here is an instructive session with an msys g++ 13.2 with a modified example (in particular, I didn't use the iostream lib but simply return something from the functions, including main, as the observable behavior). I show the two files, compile them to object files, and inspect them with the gnu binutil nm
. It shows that InlineFun()
is compiled into a normal undefined symbol at the call site and into a defined symbol at the implementation site, despite its inline declaration. Consequently, the two can be successfully linked:
$ cat perhaps-inline.cpp
inline int InlineFun();
int main(){ return InlineFun(); /* From other TU */ }
$ cat other.cpp
inline int InlineFun() { return 3; }
int Fun() { return InlineFun(); }
$ gcc -c perhaps-inline.cpp other.cpp
perhaps-inline.cpp:1:12: warning: inline function ‘int InlineFun()’ used but never defined
1 | inline int InlineFun();
| ^~~~~~~~~
$ nm -g --undefined-only perhaps-inline.o
U _Z9InlineFunv
U __main
$ nm -g --defined-only other.o
0000000000000000 T _Z3Funv
0000000000000000 T _Z9InlineFunv
$ g++ -o perhaps-inline other.o perhaps-inline.o
$ ./perhaps-inline
$ echo $?
3
Now I switch optimization on:
$ rm *.o perhaps-inline
$ gcc -O1 -c perhaps-inline.cpp other.cpp
perhaps-inline.cpp:1:12: warning: inline function ‘int InlineFun()’ used but never defined
1 | inline int InlineFun();
| ^~~~~~~~~
$ nm -g --defined-only other.o
0000000000000000 T _Z3Funv
$ g++ -o perhaps-inline other.o perhaps-inline.o
/usr/lib/gcc/x86_64-pc-msys/13.2.0/../../../../x86_64-pc-msys/bin/ld: perhaps-inline.o:perhaps-inline:(.text+0xa): undefined reference to `InlineFun()'
collect2: error: ld returned 1 exit status
Ooops. The symbol InlineFun
is gone in other.o. Consequently, the linker cannot resolve the symbol any longer which is needed by perhaps-inline.o.
As we saw above, we can simply omit the "inline" in the function definition in other.cpp; the function symbol will be emitted and the link succeeds. We can also tell the compiler not to optimize (-O0
). It will not inline at all and produce code for the function, including the symbol. The link will succeed:
$ gcc -O0 -c other.cpp
$ cat other.cpp
inline int InlineFun() { return 3; }
int Fun() { return InlineFun(); }
$ gcc -O0 -c other.cpp
$ nm -g --defined-only other.o
0000000000000000 T _Z3Funv
0000000000000000 T _Z9InlineFunv
$ g++ -o perhaps-inline perhaps-inline.o other.o
$ ./perhaps-inline
$ echo $?
3
Or shorter, first with -O0
which succeeds, with a warning, and then with -O1
, which fails at the link stage:
$ g++ -O0 -o perhaps-inline other.cpp perhaps-inline.cpp
perhaps-inline.cpp:1:12: warning: inline function ‘int InlineFun()’ used but never defined
1 | inline int InlineFun();
| ^~~~~~~~~
$ g++ -O1 -o perhaps-inline other.cpp perhaps-inline.cpp
perhaps-inline.cpp:1:12: warning: inline function ‘int InlineFun()’ used but never defined
1 | inline int InlineFun();
| ^~~~~~~~~
/usr/lib/gcc/x86_64-pc-msys/13.2.0/../../../../x86_64-pc-msys/bin/ld: /tmp/cc7eC9Lr.o:perhaps-inline:(.text+0xa): undefined reference to `InlineFun()'
collect2: error: ld returned 1 exit status
Now we'll inspect the "reached" wording. Let's call g++ differently! We let g++ read the files from standard input. Lo and behold, it can now "reach" the definition! There is no warning any longer. Effectively, this combines the source files into one translation unit.
$ rm perhaps-inline
$ cat other.cpp perhaps-inline.cpp | g++ -Wall -O1 -xc++ -o perhaps-inline -
$ ./perhaps-inline
$ echo $?
3
But we can observe a similar effect with Visual Studio: The link fails for Release mode (optimization on) but succeeds if we switch on whole program optimization: If the build system looks at all source files at once, the "translation unit" concept becomes fuzzy and the inline function from the other file becomes available for inlining.
Bottom line:
- Such a program is ill-formed if the source files are compiled separately (and thus form separate translation units).
- The compilers perform their duty and emit a diagnostic message in the form of a warning.
- In the absence of an inline definition, both compilers try to be robust and fall back to producing a regular function call, potentially resolving the missing function definition at link time. This succeeds if such a function is available (because it is defined non-inline somewhere).
- In debug mode, a compiler may still treat the function as non-inline, i.e. emit code and a symbol for the function. (This will allow the user to step into it during a debug session.) This will let the link stage succeed and mask the error (if one ignores the warning). Such a build will fail when optimization is turned on.
- A build system is free to interpret the standard's "reachable" requirement leniently as "maybe I can find it somewhere, give me a second", for example when whole program optimization is turned on.
static inline
, the compiler will report the error. By default,inline
isextern inline
, but the compiler treats it likeextern
. A compiler extension? It does no harm except creating confusion. – Sympathininline
. The only guaranteed effect ofinline
is that you can legally provide multiple definitions. It makes sense for an implementation to be lenient, only emit a warning, and simply link the definition from elsewhere. – SalazarInLineFun
or aboutostream::operator<<
? – TextualismInlineFun
– Ramrodother.cpp
does not produce a symbol that could be linked against. Indeed, no code is produce at all! The inline function does not have any observable effect, and consequently the compiler simply discards it. – Salazar