Skip to content
C++
Language
since C++98
Basic

Inline Functions

Inline functions may be defined identically across translation units and hint to the compiler to expand their body at each call site instead of using the normal call mechanism.

Inline Functionsince C++98

A function declared inline may be defined identically in multiple translation units without violating the One Definition Rule, and serves as a hint to the compiler to expand its body at each call site rather than generating a conventional function call.

Overview

The inline keyword has two distinct and largely independent effects that are often conflated.

The ODR exemption is the guaranteed effect. Normally, a function with external linkage may only be defined once across the entire program. Marking a function inline lifts this restriction: the same definition may appear in as many translation units as needed, provided every definition is token-for-token identical and the tokens carry the same meaning in each translation unit. This is what makes header-only libraries possible β€” a function defined in a .h or .hpp file can be #included into dozens of .cpp files without triggering a linker error.

The call-site expansion hint is advisory. Compilers are free to ignore it entirely, and modern optimisers routinely inline functions that carry no inline keyword while refusing to inline those that do. Profile-guided optimisation and link-time optimisation are far more reliable mechanisms for controlling inlining than the keyword itself.

Several constructs carry implicit inline status without the keyword:

  • Member functions defined inside a class or struct body (C++98)
  • constexpr functions and constructors (C++11)
  • consteval functions (C++20)
  • Function templates (whose ODR semantics already mirror inline)

Syntax

cpp
// Free function β€” explicit inline
inline double square(double x) { return x * x; }

// Member function β€” implicit inline (defined in class body)
struct Vec2 {
    float x, y;
    float length() const { return std::sqrt(x*x + y*y); }  // implicitly inline
};

// Member function β€” explicit inline outside class body
struct Matrix4 {
    float determinant() const;
};
inline float Matrix4::determinant() const { /* ... */ return 0.f; }

Examples

Type-safe replacement for function-like macros

The classic motivator: macros bypass the type system and produce surprising results with side-effecting arguments.

cpp
// Macro β€” evaluates x twice; x++ causes UB if x is the larger value
#define MAX(a, b) ((a) > (b) ? (a) : (b))

// Inline template β€” evaluated once, fully type-checked
template <typename T>
inline T max_val(T a, T b) { return a > b ? a : b; }

int main() {
    int x = 5, y = 3;
    int r1 = MAX(x++, y);      // x incremented twice β€” undefined behaviour
    int r2 = max_val(x++, y);  // x incremented exactly once β€” well-defined
}

Header-only library pattern

The ODR exemption is what makes single-header libraries viable. Place the full definition in the header; every translation unit that includes it gets its own copy, and the linker merges them.

cpp
// math_utils.hpp
#pragma once
#include <cmath>

inline double degrees_to_radians(double deg) {
    return deg * (3.14159265358979323846 / 180.0);
}

inline double hypot3(double x, double y, double z) {
    return std::sqrt(x*x + y*y + z*z);
}
cpp
// renderer.cpp
#include "math_utils.hpp"   // defines degrees_to_radians here

// geometry.cpp
#include "math_utils.hpp"   // defines degrees_to_radians here too β€” legal

Without inline, including this header in two .cpp files would cause a multiple-definition linker error.

constexpr implies inline (C++11)

cpp
// C++11 β€” constexpr implies inline; can be used in constant expressions
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

static_assert(factorial(6) == 720);  // evaluated at compile time

int runtime_n = 8;
int r = factorial(runtime_n);       // also valid at runtime

Because constexpr already implies inline, adding the inline keyword explicitly is redundant but not ill-formed.

Virtual functions and inlining

Virtual functions can be marked inline, but the compiler can only expand them when it can resolve the call statically β€” i.e., the dynamic type is known at the call site.

cpp
struct Shape {
    virtual inline double area() const { return 0.0; }  // inline is redundant here
};

struct Circle : Shape {
    double r;
    double area() const override { return 3.14159 * r * r; }
};

void process(Shape& s) {
    double a = s.area();   // virtual dispatch β€” cannot be inlined
}

void process_known(Circle& c) {
    double a = c.area();   // compiler may devirtualize and inline
}

Best Practices

Define inline functions in header files. Because every translation unit that calls an inline function must also have its definition, the natural home is a shared header. Defining an inline function in a .cpp file and calling it from another .cpp file will cause a linker error β€” the definition will be invisible to the other translation unit.

Prefer inline for small, hot accessor/mutator functions. Getters and setters that do nothing but read or write a data member are ideal candidates. The call overhead can exceed the work the function performs.

Let the compiler decide for larger functions. For functions longer than a few lines, the keyword is largely meaningless to a modern optimising compiler. Inlining a large function increases binary size and instruction-cache pressure, which can hurt performance. Trust the optimiser; use PGO or LTO when you need predictable inlining decisions.

Don't rely on inline for performance without measuring. Profile first. A function you expect to be inlined may not be; one you never considered may already be. Tools like -fopt-info-inline (GCC) or -Rpass=inline (Clang) report actual inlining decisions.

Mark constexpr functions constexpr, not just inline. constexpr implies inline and gives you compile-time evaluability as a bonus (C++11).

Common Pitfalls

Defining an inline function differently across translation units is undefined behaviour. The ODR requires identical definitions. A subtle difference β€” even a different typedef that resolves to the same type β€” makes the program ill-formed, no diagnostic required. This class of bug is notoriously hard to diagnose because the linker silently picks one definition.

cpp
// file_a.cpp
using size_t = unsigned long;
inline size_t count() { return 1UL; }

// file_b.cpp
using size_t = unsigned long long;  // different underlying type on some platforms
inline size_t count() { return 1UL; } // technically different definition β€” ODR violation

Forgetting inline on out-of-class definitions. If you declare a member function inside the class body but define it outside in a header, the out-of-class definition must carry inline. Omitting it silently removes the ODR exemption.

Recursive inline functions are never guaranteed to inline. The compiler may produce a non-inlined version for the recursive case and inline only the terminal case. Annotating a recursive function inline is usually meaningless.

inline does not mean static. An inline free function still has external linkage by default. If you want a function local to a translation unit, use static or an anonymous namespace. Confusing the two can cause subtle ODR violations when the same-named function appears in multiple TUs with the same signature but different implementations.

Inlining can increase code size unexpectedly. Each call site gets its own copy of the expanded body. For a function called from fifty places, aggressive inlining multiplies the instruction footprint by fifty β€” potentially evicting hot code from the instruction cache.

C++17: Inline Variables

C++17 extended the inline specifier to variables. Before this, placing a variable definition in a header caused an ODR violation the moment the header was included in more than one translation unit. The only escapes were extern declarations with a single out-of-line definition in a .cpp file, or static β€” which silently gives each translation unit its own independent copy, breaking pointer-identity and wasting space.

With inline variables, the same ODR exemption as for inline functions applies: all identical definitions across translation units are collapsed to a single object by the linker, so the variable has a unique address program-wide.

cpp
// config.hpp  β€” safe to include in as many .cpp files as you like
#pragma once
#include <string_view>

// Global mutable state in a header β€” legal since C++17
inline int g_request_count = 0;

// Constant usable in constant expressions AND addressable (not just a literal)
inline constexpr double kPi = 3.14159265358979323846;

// Inline static data member β€” no out-of-class definition needed
struct Registry {
    inline static int instance_count = 0;
    inline static constexpr std::size_t kMaxEntries = 1024;
};

Two important corollaries fall out of this:

  • constexpr at namespace scope is implicitly inline in C++17. Any constexpr variable in a header is already ODR-safe; writing inline constexpr is redundant but explicit.
  • inline static data members no longer need a corresponding .cpp definition. This eliminates the split between the in-class declaration and the out-of-class definition that was a perennial source of linker errors before C++17.

The rule still applies: every definition must be token-for-token identical across all translation units. Differing definitions are an ODR violation, ill-formed with no required diagnostic.

Force-Inline Hints

All forms of the inline keyword are hints to the optimiser. When you need a stronger guarantee β€” for instance, a library utility called in a tight inner loop where a function-call overhead is measurable β€” each major toolchain provides a non-portable extension.

ToolchainSyntaxNotes
GCC__attribute__((always_inline)) inlineCompiler errors if inlining is impossible
Clang[[gnu::always_inline]]Does not error if impossible; use [[gnu::gnu_inline]] extern inline to get a linker error instead
MSVC__forceinlineEmits C4714 warning (not error) when inlining fails; requires /O1 or /O2 to be meaningful
MSVC (modern)[[msvc::forceinline]]Standard-attribute syntax equivalent to __forceinline, available in VS 2019+

A portable cross-compiler macro used by projects like Mesa and fast_float:

cpp
#if defined(__clang__)
    // [[gnu::gnu_inline]] + extern causes a linker error if Clang can't inline
    #define FORCE_INLINE [[gnu::always_inline]] [[gnu::gnu_inline]] extern inline
#elif defined(__GNUC__)
    // GCC produces a compile error if always_inline fails
    #define FORCE_INLINE [[gnu::always_inline]] inline
#elif defined(_MSC_VER)
    #pragma warning(error: 4714)   // promote "failed to inline" to an error
    #define FORCE_INLINE __forceinline
#else
    #define FORCE_INLINE inline     // best-effort on unknown compilers
#endif

FORCE_INLINE int clamp(int v, int lo, int hi) {
    return v < lo ? lo : v > hi ? hi : v;
}

When the compiler may still refuse to inline even with force-inline hints:

  • Recursive functions β€” the call depth is not known at compile time.
  • Virtual functions β€” they may be dispatched through a vtable pointer at runtime.
  • Functions taking their own address β€” the symbol must exist as a callable object.
  • Debug builds β€” optimisation is typically disabled; __forceinline has no effect without /Ob1 or higher in MSVC.
  • Extremely large functions β€” some compilers have hard inlining budget limits.

Prefer [[gnu::always_inline]] over __attribute__((always_inline)) when targeting C++11 or later; the attribute syntax integrates more cleanly with modern code.

The inline keyword can only enable inlining within a single translation unit β€” the compiler must see the function body to expand it. This is why header-only libraries define functions in .hpp files rather than .cpp files.

Link-Time Optimisation (LTO, also called IPO β€” Inter-Procedural Optimisation) breaks this boundary. With LTO enabled, the compiler emits an intermediate representation (IR) rather than machine code, and the linker invokes the compiler's full optimisation pipeline across all translation units simultaneously:

bash
# GCC/Clang: basic (full) LTO β€” whole-program single-pass
g++ -O2 -flto src/a.cpp src/b.cpp -o app

# Clang ThinLTO β€” parallel, incremental, nearly as effective
clang++ -O2 -flto=thin src/a.cpp src/b.cpp -o app

# MSVC whole-program optimisation (/GL compile + /LTCG link)
cl /O2 /GL src/a.cpp src/b.cpp /link /LTCG

Under LTO, a function defined in b.cpp (without the inline keyword) can be inlined into a call site in a.cpp β€” the linker has both bodies available as IR and can make the decision. This changes the calculus for inline in several ways:

  • You no longer need inline purely for performance reasons. If you ship a compiled library alongside its sources and enable LTO, non-inline functions in .cpp files will still be inlined by the optimizer.
  • inline remains necessary for ODR exemption. LTO does not remove the requirement that definitions be unique unless explicitly marked inline. A free function defined (not just declared) in a header without inline still causes linker errors when included in two .cpp files.
  • ThinLTO (Clang -flto=thin, GCC -flto=auto) is the practical choice for large projects. It builds a global function-index at link time, processes each module in parallel, and achieves close to full-LTO performance gains (around 2.6–2.9% throughput improvement for typical C++ codebases) at roughly 1.2Γ— non-LTO link cost instead of the 5Γ— cost of basic LTO.

Diagnostic flags to inspect actual inlining decisions at compile time:

bash
# GCC: report inlining decisions
g++ -O2 -fopt-info-inline-optimized src/foo.cpp

# Clang: report what the inline pass did
clang++ -O2 -Rpass=inline src/foo.cpp

# Clang: report what the inline pass missed
clang++ -O2 -Rpass-missed=inline src/foo.cpp

Decision Guide: inline vs static vs constexpr vs LTO

Choosing the right mechanism for functions and constants in headers:

GoalRecommended approach
ODR-safe function callable from multiple TUsinline function in header
Performance-critical function, one TULTO or force-inline hint
Compile-time constant, any literal typeinline constexpr variable (C++17)
Compile-time constant, integral only, pre-C++17constexpr (implicitly inline in C++17; use enum or const pre-C++17)
Mutable global state shared across TUsinline variable in header (C++17)
Symbol intentionally local to one TUstatic function/variable or anonymous namespace
Cross-TU inlining without header definitionsLTO (-flto, /GL /LTCG)

Key distinctions to keep in mind:

  • inline vs static (free functions): Both suppress linker errors when the same header is included in multiple TUs, but for different reasons. static gives each TU its own independent copy with internal linkage β€” address comparisons across TUs will differ. inline guarantees a single definition is selected; all TUs share the same address.
  • inline constexpr vs static constexpr (namespace-scope constants): static constexpr gives each TU its own copy, so taking its address in two TUs yields two different pointers. inline constexpr (C++17) guarantees a single object. For constants used only as values (never addressed), the distinction is immaterial β€” but inline constexpr is the idiomatic C++17 choice.
  • constexpr function vs inline function: constexpr implies inline and additionally enables compile-time evaluation. Prefer constexpr for pure functions that can be evaluated at compile time; there is nothing to gain from using just inline.

Inline variables (C++17)

inline variables are the variable-level counterpart to inline functions, and solve the ODR problem for header-defined data. Before C++17 the only way to share a single mutable variable across translation units via a header was an extern declaration backed by exactly one .cpp definition β€” a two-file dance that broke down the moment the .cpp was forgotten. inline variables eliminate that ceremony.

cpp
// C++17 β€” all of these are legal in a header included by many .cpp files
#pragma once
#include <string_view>

inline int         g_active_connections = 0;       // shared mutable global
inline constexpr double kGravity = 9.80665;        // addressable compile-time constant
inline constexpr std::string_view kAppName = "bigcpp";

struct Config {
    inline static int  thread_pool_size = 4;       // no out-of-class definition needed
    inline static constexpr std::size_t kCacheLineSz = 64;
};

Pointer identity is guaranteed. Because the linker collapses all identical inline definitions to a single object, &kGravity is the same address in every translation unit β€” a property static constexpr does not provide.

cpp
// C++17 β€” demonstrating address uniqueness
// header.hpp
inline constexpr int kMagic = 42;

// a.cpp
#include "header.hpp"
const int* a_ptr() { return &kMagic; }

// b.cpp
#include "header.hpp"
const int* b_ptr() { return &kMagic; }

// main.cpp β€” a_ptr() == b_ptr() is guaranteed with inline; not with static constexpr
#include <cassert>
assert(a_ptr() == b_ptr());

Inline namespaces for versioning

inline namespace makes its members visible in the enclosing namespace while preserving ABI-safe versioning. The inline qualifier on a namespace is a different feature from the inline specifier on functions and variables, but it participates in the same ODR and linkage machinery and is commonly grouped with inline functions in reference material.

cpp
// C++11 β€” library versioning with inline namespaces
namespace mylib {
    inline namespace v2 {           // v2 is the "current" ABI
        struct Widget { int id; int flags; };
        void process(Widget w);
    }
    namespace v1 {                  // v1 available for explicit opt-in
        struct Widget { int id; };
        void process(Widget w);
    }
}

// Callers spelling mylib::Widget get the v2 definition automatically.
// Old code that explicitly says mylib::v1::Widget still compiles.
mylib::Widget w{1, 0};             // resolves to mylib::v2::Widget
mylib::v1::Widget old{1};          // explicit v1 access

Name lookup, ADL, and using namespace all see through the inline namespace transparently. The inline namespace name still appears in the mangled symbol, so old and new ABIs remain link-time distinct even though source-level access is uniform.

ODR and inline definitions

The One Definition Rule treats inline functions as the canonical exception to "one definition per program". An inline function may be defined in every translation unit that uses it, subject to the rule that all definitions are identical β€” token-for-token, with tokens carrying identical meaning in every TU.

cpp
// fine_header.hpp β€” identical in every TU that includes it
inline int add(int a, int b) { return a + b; }

// odr_violation.hpp β€” DO NOT DO THIS
// If two TUs include this with different NDEBUG states the definitions differ β†’ ODR violation
#ifdef NDEBUG
inline int checked_add(int a, int b) { return a + b; }
#else
#include <stdexcept>
inline int checked_add(int a, int b) {
    if (b > 0 && a > INT_MAX - b) throw std::overflow_error("overflow");
    return a + b;
}
#endif

ODR violations are ill-formed, no diagnostic required. The linker is permitted (but not required) to pick either definition silently. The resulting behaviour is undefined; crashes or wrong answers can appear sporadically depending on link order.

The practical rule: never conditionalize the body of an inline function on a macro that might differ between translation units (NDEBUG, platform guards, _DEBUG). Use a non-inline function in a .cpp file instead, or make both branches produce bitwise-identical code.

Inlining in debug vs release builds

Debug builds typically disable inlining entirely, making inline a release-only optimisation. This has concrete implications for debugging, binary size, and correctness testing.

bash
# GCC/Clang debug: -O0 disables the inliner; -g adds debug symbols
g++ -O0 -g -o app_debug src/main.cpp

# GCC/Clang release: -O2 or -O3 enables aggressive inlining
g++ -O2 -DNDEBUG -o app_release src/main.cpp

# Clang: selectively enable inlining at -O0 (rare; for profiling harnesses)
clang++ -O0 -mllvm -inline-threshold=0 -o app src/main.cpp

__forceinline / [[gnu::always_inline]] are ignored at -O0 on most compilers. MSVC requires at least /Ob1; GCC/Clang with -O0 will still emit a regular call even if the attribute is present, unless -O1 is applied first.

cpp
// C++11 β€” force-inline is a no-op at -O0 in most compilers
[[gnu::always_inline]] inline int fast_abs(int x) {
    return x < 0 ? -x : x;
}
// At -O0: fast_abs is called normally; breakpoints inside work fine.
// At -O2: call site replaced with the body; no frame for fast_abs in the debugger.

This means a function that behaves correctly in a debug build can still trigger undefined behaviour or performance regressions in release if aggressive inlining changes observable timing or exposes aliasing violations that were previously hidden by function-call boundaries.

Profile-guided inlining

Profile-guided optimisation (PGO) lets the compiler use runtime frequency data to make inlining decisions that static heuristics cannot. The inline keyword is irrelevant to PGO; it acts on every function in the program based on actual call-site heat.

bash
# Step 1 β€” instrument build
g++ -O2 -fprofile-generate -o app_instr src/main.cpp

# Step 2 β€” run representative workload (generates *.gcda profile data)
./app_instr --bench realistic_input.dat

# Step 3 β€” optimised build using profile data
g++ -O2 -fprofile-use -fprofile-correction -o app_pgo src/main.cpp

# Clang equivalent (uses .profraw β†’ .profdata pipeline)
clang++ -O2 -fprofile-instr-generate -o app_instr src/main.cpp
./app_instr --bench realistic_input.dat
llvm-profdata merge -output=default.profdata default.profraw
clang++ -O2 -fprofile-instr-use=default.profdata -o app_pgo src/main.cpp

PGO typically improves throughput 5–15% on real workloads, primarily through better inlining decisions: hot call sites that exceed the compiler's default size threshold get inlined; cold paths that were previously inlined (wasting icache) get outlined. Neither effect is achievable by annotating individual functions with inline or force-inline hints.

A complementary technique is FDO + AutoFDO (Google's sampling-based PGO), which collects profiles from production binaries without instrumentation overhead, then feeds them back into subsequent builds.

constexpr functions and inlining

constexpr implies inline and additionally requires the function to be evaluable at compile time when called with constant arguments. This makes constexpr strictly superior to bare inline for any pure function β€” it gives you the ODR exemption, the call-site expansion hint, and compile-time evaluation all at once.

cpp
// C++11 β€” constexpr implies inline
constexpr long long fibonacci(int n) {
    return n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}

static_assert(fibonacci(10) == 55);         // compile-time: no runtime cost
constexpr long long fib20 = fibonacci(20);  // also compile-time

int dynamic_n = 30;
long long r = fibonacci(dynamic_n);         // runtime: normal function call
cpp
// C++17 β€” constexpr if, multiple return paths, still inline-implied
constexpr double fast_pow(double base, int exp) {
    if (exp == 0) return 1.0;
    if (exp % 2 == 0) {
        double half = fast_pow(base, exp / 2);
        return half * half;
    }
    return base * fast_pow(base, exp - 1);
}

static_assert(fast_pow(2.0, 10) == 1024.0);
cpp
// C++20 β€” consteval forces compile-time-only evaluation
consteval int must_be_compile_time(int n) { return n * n; }

constexpr int r1 = must_be_compile_time(7);  // OK β€” evaluated at compile time
// int x = 5;
// int r2 = must_be_compile_time(x);         // error β€” x is not a constant expression

The key caveat: a constexpr function called at runtime is simply an inline function β€” the compiler is free to inline or not. For truly forcing compile-time evaluation use consteval (C++20) or assign the result to a constexpr variable.

See Also

  • Inline Variables β€” C++17 extended the inline specifier to variables, enabling the same ODR exemption for global and static data members in headers.
  • Virtual Functions β€” Virtual dispatch prevents inlining except when the compiler can statically determine the dynamic type.
  • Special Member Functions β€” Compiler-generated special member functions (constructors, destructors, copy/move operations) are implicitly inline when defaulted inside the class body.