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.

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.