Why having a function declared as inline without a definition results in a linker error?
Asked Answered
R

1

6

I know that a function declared as inline must have a definition in its own translation unit (As the standard says: An inline function shall be defined in every translation unit in which it is used).

Previously, my understanding to this requirement was that the compiler will try to inline a function declared with the inline keyword, and thus although inlining is not compulsory, the definition should be visible to the compiler in the first place.

Following this understanding, I thought not providing the definition should cause a compiler error. In reality, I've never written an inline function without a definition. But then I tried this error intentionally in my toy program, and it causes a linker error.

So, I'm just curious why doesn't C++ keep this problem as a compile-time error and let the compiler catch the error? And since, in my understanding, inline function has external linkage by default, how exactly does the linker catch this error behind the scenes?


An example:

// main.cpp:

inline void InlineFun();

int main()
{
    InlineFun();
}

// other.cpp:

#include<iostream>
#include<string>

inline void InlineFun()
{
    std::cout << "From other TU" << std::endl;
}

I'm using Visual Studio 2022 on Windows 10 with C++20. The error I encounter are LNK2001 and LNK1120. The examples provided in the comments are also perfectly fine.


A follow-up example:

// main.cpp:

inline void InlineFun();

int main()
{
    InlineFun();  // From other TU
}

// other.cpp:

#include<iostream>
#include<string>

inline void InlineFun()
{
    std::cout << "From other TU" << std::endl;
}

void Fun()
{
    InlineFun();
}

This time, using the same environment, I added a normal function Fun in other.cpp to try to force InlineFun to have observable effect. My program then compiled and linked successfully (with no warnings or errors reported on Visual Studio) and printed From other TU, seemingly disregarding the standard.

Ramrod answered 25/1, 2024 at 7:15 Comment(15)
What did you try and what error did you get? Please post a minimal reproducible example.Cheloid
@Cheloid this isnt about one specific error and they are not seeking for a fix. I think the description is clear enough. An example could be this godbolt.org/z/Pfe3sr1s7Boozy
I don't think the C++ specification distinguishes between the compiler and linker. As long as you eventually get an error it's following the spec.Throughput
It seems to throw a warning though. Also, inlining magic seems to not happen at -O0, I see it happening at -O1 or greater level. godbolt.org/z/Ehv384PKoCheloid
@Throughput Regarding compiler and linker, I've also just noticed that linkers are not unique to C++. So isn't catching errors earlier at compile-time benefits the standard in general?Ramrod
The standard doesn't require that implementations report errors in any particular way, so long as they're reported. They can be reported early or late. As far as it's concerned, compilation+linking is one operation.Throughput
Not an answer, just an additional observation: if you declare your function as static inline, the compiler will report the error. By default, inline is extern inline, but the compiler treats it like extern. A compiler extension? It does no harm except creating confusion.Sympathin
The C++ standard doesn't refer to "compiler" or "linker" in normative text at all (notes and footnotes that do mention them are non-normative). So an implementation that has no "compiler" and no "linker" is just as acceptable to the standard as a toolchain with a distinct "compiler" executed before a "linker". There is no notion of "producing errors early" in the standard (e.g. in the "compiler" rather than the "linker") - the only requirement is that a diagnostic is produced.Skive
Also note that whether the compiler (attempts to) inline or not is entirely independent of the inline. The only guaranteed effect of inline 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.Salazar
What is the linker error you get, is about InLineFun or about ostream::operator<<?Textualism
@Textualism It's about InlineFunRamrod
@Throughput Re "errors can be reported early or late": The violation is reported early, by the compiler, with a warning. The eventual linker error is only indirectly related to that -- as detailed in my answer, it's because the inline-declared function in other.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
@Peter-ReinstateMonica Interesting point... I added a function to let the inline function have observable effect, and my program suddenly runs. Please see my follow-up example aboveRamrod
@Ramrod Interesting. Indeed, VS (2022 here) does not warn at all, but only if "Whole program optimization" is on -- this is probably the reason for "reachable" in the standard. The inline definition is "reachable" from any file in the project if that option is on. The linker error reappears when the option is switched off. gcc compiles and links your new example with the default settings but not with -O3!Salazar
I have trouble reproducing this -- now, VS does not link any longer, no matter which options (it's kindof infamous in my experience for using stale binaries in builds, but this is actually the opposite. Hm). By the way, you may be interested in the easy-to-understand code gcc and clang produce with even modest optimizations for your new other.cpp ;-). Not much to link against now, is there.Salazar
S
5

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.
Salazar answered 25/1, 2024 at 14:5 Comment(5)
Could you please elaborate a little bit more on how "The symbol is simply not exported (to linker?)" while inline function has external linkage? I think there is some subtlety underlies here that makes me confused. Thanks for your answer :)Ramrod
@Ramrod Hopefully, inspecting the object files has made it clearer.Salazar
"Reachable from the translation unit where it is odr-used" — the particular language doesn't require the definition to appear in the transition unit; it could be reachable through an extern declaration. Right?Sympathin
@Sympathin No, that is not what is meant. The "definition" (as opposed to a (possibly extern) "declaration" is the source code which an extern declaration does not provide. The compiler must "see" the function implementation. Traditionally that means it is in the same "translation unit" (the source after preprocessing, in particular after processing includes). It may be debatable if gcc caller.c implementer.c counts (after all, cat implementer.c caller.c | gcc would, I think); I suppose the standard language is a bit fuzzy to cover such cases.Salazar
@Sympathin The standard is, of course, careful to not preclude build systems which deviate from the traditional -nix TU compiler/linker model; one could imagine repository based systems or, simpler, IDEs which give the compiler/linker "system" access to basically all source files. Hell, there are interpreters out there, aren't there? And I'm not sure how modules come into play. I think the language covers these cases where "reachable" is through an uncommon or proprietary or future language mechanism.Salazar

© 2022 - 2025 — McMap. All rights reserved.