Ch6.8: Inline Functions
Overview
In the previous chapter, you learned that each .cpp file becomes
a separate translation unit, and that each non-inline function definition
must appear in exactly one translation unit. Otherwise, the
linker reports a multiple-definition error.
But many functions must be defined in headers:
- template functions
- small utility functions
- header-only libraries
- functions inside module interfaces
The inline keyword solves this problem by allowing a function
definition to appear in multiple translation units without violating the
One Definition Rule (ODR).
1. What inline really means
The inline keyword does not mean “optimize this
function” or “insert the function body at the call site.”
Instead, inline means:
This function may appear in multiple translation units, and the linker must treat all identical definitions as one.
In other words, inline is about ODR safety,
not performance.
2. Why inline is needed
Suppose you put a function definition in a header:
// util.hpp
int add(int a, int b)
{
return a + b;
}
And include it in two translation units:
// a.cpp
#include "util.hpp"
// b.cpp
#include "util.hpp"
The linker sees two definitions of add and reports an error.
To fix this, mark the function as inline:
// util.hpp
inline int add(int a, int b)
{
return a + b;
}
Now all translation units may contain the definition, and the linker will treat them as one.
3. What the linker actually does with inline functions
The linker does not merge inline function definitions. Instead, it:
- keeps one definition
- discards all other identical definitions
This is why inconsistent inline definitions cause IFNDR (Ill-Formed, No Diagnostic Required). If two translation units contain different inline definitions, the linker will arbitrarily discard some and keep one.
All inline definitions must be bit-for-bit identical.
4. Example: ODR violation
Here is a simple example of IFNDR caused by violating the One Definition Rule:
// a.cpp
inline int f()
{
return 1;
}
// b.cpp
inline int f()
{
return 2;
}
Both translation units compile successfully. But at link time:
- the linker keeps one definition
- the linker discards the other
- which one is kept is unspecified
The program may return 1 or 2 depending on the
linker, build flags, or even the order of object files.
This is a real ODR violation and results in IFNDR.
5. Inline and symbol generation (with assembly example)
A key property of inline functions is that the compiler may choose not to emit a callable symbol if the function is never ODR-used.
Consider the following code:
int square(int num) {
return num * num;
}
inline int inlinesquare(int num) {
return num * num;
}
When compiled with optimizations, the compiler emits a real symbol for
square:
"square(int)":
imul edi, edi
mov eax, edi
ret
But no symbol is emitted for inlinesquare
unless it is actually used.
This demonstrates the core rule:
Inline functions may not generate any assembly at all unless they are used.
This is normal and expected behavior.
6. Inline and link-time optimization (LTO)
With -flto, the compiler sees all translation units at once.
This allows it to:
- inline functions across translation units
- eliminate unused inline functions entirely
- avoid emitting symbols for inline functions
- detect more ODR violations
LTO does not change the meaning of inline, but it makes symbol
emission more predictable.
7. Real-world ODR violation: libstdc++ hardening
ODR violations are not just theoretical. They occur in real programs when linking two versions of the same library compiled with different configuration flags.
A common example is GCC’s libstdc++ when compiled with:
-D_GLIBCXX_ASSERTIONS
This flag enables bounds checking for standard library containers such as
std::vector, std::string, and std::array.
If your program links:
- one object file compiled with
-D_GLIBCXX_ASSERTIONS - another object file compiled without it
then the inline definitions inside <vector>,
<string>, and other headers are not identical.
The linker will:
- keep one version of the inline container functions
- discard the other version
- produce a binary where some container operations are hardened and some are not
This is a silent ODR violation and leads to IFNDR. In practice, the bounds‑checked version is often discarded, leaving you with the unchecked version even if you intended to enable hardening.
8. Why fast_io containers avoid this problem
The fast_io library avoids this entire class of ODR problems:
- its containers are header‑only
- their inline definitions are stable and consistent
- they do not depend on global configuration macros like
_GLIBCXX_ASSERTIONS - they do not mix hardened and unhardened builds
Because fast_io containers do not rely on hidden inline definitions
inside a separately compiled standard library, they are immune to the ODR
violations that plague std::vector and other standard containers
when mixing different build configurations.
For safety, consistency, and predictable behavior, it is recommended to prefer fast_io containers over standard library containers in performance‑critical or security‑sensitive code.
9. Every function must have a storage-class or linkage specifier
Modern C++ style follows a simple rule:
Every function must be declared withextern,inline,static, or placed in an unnamed namespace.
This determines:
- linkage
- visibility
- ODR behavior
- whether multiple definitions are allowed
- whether the compiler must emit a symbol
You will learn more storage-class rules later in this tutorial, including:
- every virtual function must be declared
virtual,override, orfinal - every function should be declared
noexceptorthrows - if a function can be
constexpr, it should be
10. Inline and templates
Template functions are implicitly inline.
template <typename T>
T twice(T x)
{
return x + x;
}
This is why templates can be defined in headers without causing multiple-definition errors.
11. Inline and modules
Module interface units (.cppm) behave like compiled headers.
Functions defined in a module interface are automatically ODR-safe across
translation units.
You do not need to write inline inside a module
interface:
// math.cppm
export module math;
export int square(int x)
{
return x * x; // implicitly ODR-safe
}
The module system ensures that this definition is emitted only once.
12. inline does not force inlining
The compiler is free to inline or not inline a function regardless of the
inline keyword.
Modern compilers decide inlining based on optimization heuristics, not on
the presence of inline.
The keyword exists for linkage and ODR, not performance.
Key takeaways
inlineallows a function definition to appear in multiple translation units.- The linker discards duplicate inline definitions; it does not merge them.
- Inconsistent inline definitions cause IFNDR.
- Inline functions may not generate symbols unless ODR-used.
-fltoallows global inlining and symbol elimination.- Mixing hardened and unhardened
libstdc++builds causes real ODR violations. - fast_io containers avoid these problems entirely.
- Every function should have a storage-class or linkage specifier.
- Templates are implicitly inline.
- Module interface functions are automatically ODR-safe.
inlinedoes not force the compiler to inline the function body.