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:

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:

  1. keeps one definition
  2. 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 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:

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:

then the inline definitions inside <vector>, <string>, and other headers are not identical.

The linker will:

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:

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 with extern, inline, static, or placed in an unnamed namespace.

This determines:

You will learn more storage-class rules later in this tutorial, including:

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