Do you know the inline keyword in C/C++? Of course yes! OK let’s make a simple test. create a new test.c file and enter the codes below.

inline void func() {}

void call() { func(); }

int main(void) { call(); }

What will happen if execute gcc test.c? The answer may not be same as you thought. One possible output is,

Undefined symbols for architecture x86_64:
  "_func", referenced from:
      _call in ccNiuCj3.o
ld: symbol(s) not found for architecture x86_64
collect2: error: ld returned 1 exit status

Feel confusing? If we add a argument -O2, using gcc -O2 test.c we can compile test.c (At least I can). What happened just now?

Well, inline is not a rarely used keyword. And it can be assumed that most of the C/C++ programmers know that inline suggests the compiler to inline the specific function, but whether this function is actually inlined or not are totally depending on compiler. Once a function be inlined, the commands expand at where it is invoked, so the processor execute theses commands directly instead of manipulating the call stack.

However, there is another use for inline, that is, to alter the function’s linking (but it doesn’t change the linkage). Since inline can only suggests but can not force the compiler to inline a function. Altering linking is actually more important.

It might be much more complicated than you imagined. And its effect is even different between C and C++. Let’s introduce C++’s inline at first because I consider it’s easier to understand than C’s.

C++’s ODR (one-definition-rule)

Define noninline functions or objects exactly once across all files, and define classes, inline functions, and inline variables at most once per translation unit, making sure that all definitions for the same entity are identical.

Essentially, a translation unit is the result of applying the preprocessor to a file you feed to your compiler. The preprocessor drops sections of code not selected by conditional compilation directives (#if, #ifdef, and friends), drops comments, inserts #included files (recursively), and expands macros.

These quotations from the C++ Templates: The Complete Guide introduced the translation unit and ODR in C++. Normally we define the functions in source files and declare the functions in headers for avoiding symbols redefinition. But inline functions are exceptions, we can define a function in a header with inline specified. this header can be referenced by multiple sources files without linking errors.

We’ve mentioned before that the inline specifier doesn’t change a function’s linkage. Unless the function declared by static is internal linkage, a inline function is still external linkage. Actually, the compiler generates a symbol in each translation unit. Then when linking, the linker selects one of these symbols and discards the rest. ODR says we must ensure that all definitions for the same entity are identical, but the compiler/linker may not check it. For example, we have a a.cc and a b.cc.

// a.cc
// a.cc
#include <stdio.h>

inline int func() { return 1; }

int call();

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

// b.cc
inline int func() { return 2; }

int call() { return func(); }

Use -c argument we can let g++ complier a single translation unit without linking. At first we execute g++ a.cc -c; g++ b.cc -c to get two object files: a.o and b.o. Then use g++ -o out a.o b.o, we may get an executable which prints 1 1. But if we adjust the linking order, use g++ -o out b.o a.o instead, it may prints 2 2!

What about C?

Now let’s transfer the above codes from C++ to C. rename the files with .c suffix and compile them with gcc instead of g++. g++ a.cc -c; g++ b.cc -c, OK. gcc -o out a.o b.o? Oops, We get a linking error.

/usr/bin/ld: a.o: in function `main':
a.c:(.text+0x1b): undefined reference to `func'
/usr/bin/ld: b.o: in function `call':
b.c:(.text+0xa): undefined reference to `func'
collect2: error: ld returned 1 exit status

It seems that we cannot find the definition of func, which is our inline function. To know what happened, we need to use explore the details in those two object files. Use objdump to print the symbol table.

$ objdump -t a.o 

a.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 a.c
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss   0000000000000000 .bss
0000000000000000 l    d  .rodata        0000000000000000 .rodata
0000000000000000 l    d  .note.GNU-stack        0000000000000000 .note.GNU-stack
0000000000000000 l    d  .eh_frame      0000000000000000 .eh_frame
0000000000000000 l    d  .comment       0000000000000000 .comment
0000000000000000 g     F .text  0000000000000040 main
0000000000000000         *UND*  0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000         *UND*  0000000000000000 call
0000000000000000         *UND*  0000000000000000 func
0000000000000000         *UND*  0000000000000000 printf

$ objdump -t b.o

b.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 b.c
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss   0000000000000000 .bss
0000000000000000 l    d  .note.GNU-stack        0000000000000000 .note.GNU-stack
0000000000000000 l    d  .eh_frame      0000000000000000 .eh_frame
0000000000000000 l    d  .comment       0000000000000000 .comment
0000000000000000 g     F .text  0000000000000010 call
0000000000000000         *UND*  0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000         *UND*  0000000000000000 func

the symbols of func in both two files are marked as UND, which means no definition. Since the linker cannot find a definition in either file, it occurs an error of “undefined reference”.

Wait a second, are we really ensure that an actually inlined function need a symbol? It should expands at where it is invoked and we don’t even need a call! In fact when we compiled these two translation units, gcc didn’t inline the function at all, it just treated func as a normal noninline function. Luckily, we can use -O2 to make gcc optimizes our codes so that func will actually be inlined. After all it’s more efficient without calling.

We use g++ a.cc -c -O2; g++ b.cc -c -O2 to regenerate the object files. And check the symbol tables again.

$ objdump -t a.o

a.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 a.c
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss   0000000000000000 .bss
0000000000000000 l    d  .rodata.str1.1 0000000000000000 .rodata.str1.1
0000000000000000 l    d  .text.startup  0000000000000000 .text.startup
0000000000000000 l    d  .note.GNU-stack        0000000000000000 .note.GNU-stack
0000000000000000 l    d  .eh_frame      0000000000000000 .eh_frame
0000000000000000 l       .rodata.str1.1 0000000000000000 .LC0
0000000000000000 l    d  .comment       0000000000000000 .comment
0000000000000000 g     F .text.startup  0000000000000027 main
0000000000000000         *UND*  0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000         *UND*  0000000000000000 call
0000000000000000         *UND*  0000000000000000 printf

$ objdump -t b.o

b.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 b.c
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss   0000000000000000 .bss
0000000000000000 l    d  .note.GNU-stack        0000000000000000 .note.GNU-stack
0000000000000000 l    d  .eh_frame      0000000000000000 .eh_frame
0000000000000000 l    d  .comment       0000000000000000 .comment
0000000000000000 g     F .text  0000000000000006 call

The symbol of func disappeared! Why don’t we try to link them again? Now the linker can build the executable as we expected. And what about its output? Will its output is uncertain as in C++? Well I can confirm it prints 1 2 only whatever you change the linking order. Since there is no symbol, the invoking of func can only corresponds to the codes in the same translation unit. So invoke func in a.c will return 1, and invoke func in b.c will return 2. That is, the output is definitely 1 2.

The compiler decides whether inline a function. But if it is not inlined, it occurs error. Why doesn’t GCC generate symbol definition for inline declared function? Wikipedia1 says.

In C99, a function defined inline will never, and a function defined extern inline will always, emit an externally visible function. Unlike in C++, there is no way to ask for an externally visible function shared among translation units to be emitted only if required.

In C, the inline specifier, like in C++, doesn’t change the linkage as well. If we hope the codes can be compiled anyway while keeping the external linkage, we have to let the compiler generate the definition. So let’s try extern inline. But if we simply replace all inline by extern inline then we will get a familiar error of “multiple definition”.

So the extern inline in C does’n equivalent to the inline in C++, because of the stronger ODR restriction. In cppreference2 it says.

If an identifier with external linkage is used in any expression other than a non-VLA, (since C99) sizeof, or _Alignof (since C11), there must be one and only one external definition for that identifier somewhere in the entire program.

So when we use extern inline we must only keep one definition in the entire program. But if we hope the function actually be inlined, it must be visible in each translation unit. If there is only declaration within a translation unit then the function is impossibly be inlined in it. By looking up the Wikipedia1, we found a good practice.

A function defined inline requires exactly one function with that name somewhere else in the program which is either defined extern inline or without qualifier. If more than one such definition is provided in the whole program, the linker will complain about duplicate symbols. If, however, it is lacking, the linker does not necessarily complain, because, if all uses could be inlined, it is not needed. But it may complain, since the compiler can always ignore the inline qualifier and generate calls to the function instead, as typically happens if the code is compiled without optimization. (This may be the desired behavior, if the function is supposed to be inlined everywhere by all means, and an error should be generated if it is not.) A convenient way is to define the inline functions in header files and create one .c file per function, containing an extern inline declaration for it and including the respective header file with the definition. It does not matter whether the declaration is before or after the include.

Let’s see what happens if we do this.

  1. For the translation unit which contains the declaration, it also includes the header with the definition. So there will be a symbol definition in this translation unit.
  2. For any other translations unit which includes the header with the definition. If all the function invokes are inlined, then there is no need a symbol. Otherwise there will be a symbol reference which will point to the definition in (1.

Well honestly speaking here is one thing I didn’t figure out. Is it really need to use extern inline to declare rather than not using any qualifiers? I asked on StackOverflow but I didn’t get answer so far. You can find my question by this link Is C99’s extern inline redundant?.

Before C99, there is no definition about inline in C standard. GNU89 gives the support but the semantics of inline and extern inline are exactly opposite of those in C99. Let’s check the GNU document3.

When an inline function is not static, then the compiler must assume that there may be calls from other source files; since a global symbol can be defined only once in any program, the function must not be defined in the other source files, so the calls therein cannot be integrated. Therefore, a non-static inline function is always compiled on its own in the usual fashion.

If you specify both inline and extern in the function definition, then the definition is used only for inlining. In no case is the function compiled on its own, not even if you refer to its address explicitly. Such an address becomes an external reference, as if you had only declared the function, and had not defined it.

This combination of inline and extern has almost the effect of a macro. The way to use it is to put a function definition in a header file with these keywords, and put another copy of the definition (lacking inline and extern) in a library file. The definition in the header file causes most calls to the function to be inlined. If any uses of the function remain, they refer to the single copy in the library.

Conclusion

  • In C/C++, inline suggests the compiler inline a function but cannot force it.
  • In C/C++, inline alters the linking of a function but the linkage stays external.
  • In C++, inline allows us to define a function exactly once in each translation unit instead of defining exactly once in entire program.
  • In C99, define with inline will never, and with extern inline will always, emit a symbol definition. Which is opposite to GNU89.
  • There is no need a symbol if a function is actually inlined.
  • Define inline specified function in header, but create a declaration in one .c file which includes the respective header. So that the linker can find the symbol definition once some invokes are not actually inlined.