Quantcast
Channel: User Peter - Reinstate Monica - Stack Overflow
Viewing all articles
Browse latest Browse all 223

Answer by Peter - Reinstate Monica for Why having a function declared as inline without a definition results in a linker error?

$
0
0

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.cppinline int InlineFun() { return 3; }int Fun() { return InlineFun(); }$ gcc -c -O1 other.cpp$ nm -g --defined-only other.o0000000000000000 T _Z3Funv$ cat perhaps-inline.cppinline int InlineFun();int main(){ return InlineFun(); /* From other TU */ }$ gcc -O1 -c perhaps-inline.cppperhaps-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.cppint InlineFun() { return 3; }int Fun() { return InlineFun(); }$ gcc -c -O1 other.cpp$ nm -g --defined-only other.o0000000000000006 T _Z3Funv0000000000000000 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.cppinline int InlineFun();int main(){ return InlineFun(); /* From other TU */ }$ cat other.cppinline int InlineFun() { return 3; }int Fun() { return InlineFun(); }$ gcc -c perhaps-inline.cpp other.cppperhaps-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.o0000000000000000 T _Z3Funv0000000000000000 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.cppperhaps-inline.cpp:1:12: warning: inline function ‘int InlineFun()’ used but never defined    1 | inline int InlineFun();      |            ^~~~~~~~~$ nm -g --defined-only other.o0000000000000000 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.cppinline int InlineFun() { return 3; }int Fun() { return InlineFun(); }$ gcc -O0 -c other.cpp$ nm -g --defined-only other.o0000000000000000 T _Z3Funv0000000000000000 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.cppperhaps-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.cppperhaps-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.

Viewing all articles
Browse latest Browse all 223

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>