Inline in C++ is a keyword and linkage feature that lets you define small functions (and variables since C++17) in headers without violating the One Definition Rule, while giving the compiler freedom to inline calls for performance. It does not force inlining; the optimizer ultimately decides whether to inline a call.
This beginner-friendly guide explains what Inline in C++ really means, how to use it (functions, methods, variables), what it does and doesn’t do, performance implications, and best practices—with commented code, outputs, and “Try it yourself” challenges after each section.
🔹 What does Inline in C++ mean?
Historically, inline
suggested to the compiler “please substitute the function body at the call site.” Modern C++ gives it a precise semantic role: allow multiple identical definitions across translation units (typically in headers) without violating the One Definition Rule (ODR). Actual inlining (call substitution) is an optimization that the compiler may or may not perform.
- Semantic effect: Allows defining the same function in multiple translation units (e.g., in headers) without linker errors, as long as all definitions are identical.
- Optimization hint: The compiler may inline calls, but
inline
does not guarantee it. - Applies to: Free functions, member functions, and since C++17, variables (
inline
variables).
Try it yourself
- In one sentence, describe the difference between the semantic role of
inline
and the optimization act of “inlining.” - Find a small utility function in your code that’s defined in a header but not marked
inline
. Would it cause a multiple-definition linker error?
🔹 Basic syntax: inline functions in headers
Defining small functions in headers is common. Mark them inline
(or define them inside a class) to satisfy the ODR and avoid multiple-definition errors at link time.
// math_utils.hpp
#pragma once
inline int square(int x)
{ // inline function in a header
return x * x;
}
struct Point {
int x, y;
int sum() const
{ // member functions defined inside the class
return x + y; // are implicitly inline
}
};
// main.cpp
#include <iostream>
#include "math_utils.hpp"
using namespace std;
int main() {
Point p{3, 4};
cout << square(5)
<< "\n"; // 25
cout << p.sum()
<< "\n"; // 7
}
Output
25
7
Note: You can include math_utils.hpp
in multiple .cpp files and link them without duplicate symbol errors, thanks to Inline in C++ semantics.
Try it yourself
- Create two .cpp files that both include the header and call
square
. Verify linking succeeds wheninline
is present. - Remove
inline
and watch your linker complain about multiple definitions (then restore it).
🔹 What inline does and does not do
Behavior | inline keyword |
---|---|
Allows multiple identical definitions across TUs (ODR-friendly) | Yes |
Forces call substitution (guaranteed inlining) | No |
Improves performance automatically | Not guaranteed |
Works on functions and (since C++17) variables | Yes |
Replaces need for templates/headers | No (compliments header-based code) |
Inlining decisions consider function size, optimization level, recursion, exception paths, and code size. Compilers can inline functions without the keyword and can ignore it when it hurts performance.
Try it yourself
- Add a loop to
square
to make it larger and inspect (via compiler reports or disassembly) whether it still inlines at -O2 or -O3. - Remove
inline
yet keep the function body in the header. Does your build still link? Why (hint: “multiple definition”)?
🔹 Inline variables (C++17+)
// config.hpp
#pragma once
#include <string>
inline const int kMaxUsers = 1024; // one definition across all TUs
inline const std::string kAppName = "Demo";
// use_config.cpp
#include <iostream>
#include "config.hpp"
using namespace std;
int main() {
cout << kAppName << " supports up to "
<< kMaxUsers << " users\n";
}
Output
Demo supports up to 1024 users
Without inline
, defining globals in headers leads to multiple definition errors. Inline in C++ fixes that while preserving a single program-wide instance.
Try it yourself
- Add another .cpp that includes
config.hpp
. Build and link. Then removeinline
and observe the linker error. - Make
kMaxUsers
non-const and try modifying it from two TUs. What happens? Why avoid mutable globals?
🔹 constexpr and templates are implicitly inline
// implicit_inline.hpp
#pragma once
template <typename T>
T add(T a, T b)
{ // function template in a header → implicitly inline
return a + b;
}
constexpr int triple(int x)
{ // constexpr → implicitly inline
return 3 * x;
}
// main.cpp
#include <iostream>
#include "implicit_inline.hpp"
using namespace std;
int main() {
cout << add(2, 3) << "\n"; // 5
cout << triple(7) << "\n"; // 21
}
Output
5
21
Templates must live in headers so all translation units can instantiate them. Their header definitions are ODR-safe because they’re implicitly inline.
Try it yourself
- Add
inline
to theadd
template anyway; confirm it compiles (it’s redundant but allowed). - Move the template definition into a .cpp and include only the declaration in a header. Observe linker or unresolved symbol issues.
🔹 Inline vs macros (and why inline wins)
Macros are preprocessor text substitutions; inline functions are typed, scoped, debuggable, and safer. Prefer inline functions for utilities and small operations.
Aspect | Macro | Inline function |
---|---|---|
Type safety | No | Yes (checked by compiler) |
Scope/namespace | Global textual | Respects C++ scopes/namespaces |
Debugging | Hard | Better (symbols, stepping) |
ODR/linkage | Text only | ODR-safe via inline semantics |
Side effects | Risky evaluation | Well-defined parameter eval |
// Prefer this:
inline int max_i(int a, int b) {
return (a > b) ? a : b;
}
// Avoid this: may evaluate arguments multiple times
#define MAX_I(a,b) ((a)>(b)?(a):(b))
Try it yourself
- Create
MAX_I(i++, j++)
and observe double increments. Replace withmax_i
and confirm correct single evaluation. - Use
decltype(auto)
and templates to write a typedmax_t
inline function that works with any comparable type.
🔹 Performance, inlining decisions, and code size
Inlining removes call overhead and exposes more optimization opportunities (constant propagation, vectorization). But it increases code size (instruction cache pressure). Modern compilers inline aggressively at -O2
/-O3
when profitable, regardless of the inline
keyword. Link-Time Optimization (LTO) can inline across translation units.
- Small, frequently called functions benefit most.
- Large or recursive functions are rarely inlined.
- Virtual calls are usually not inlined unless devirtualized (e.g., via
final
, whole-program analysis). - Non-portable attributes like
[[gnu::always_inline]]
exist but should be used sparingly.
#include <chrono>
#include <iostream>
using namespace std;
inline int add_inline(int a, int b)
{ return a + b; }
int add_normal(int a, int b)
{ return a + b; }
int main() {
// prevent optimization away
volatile long long sink = 0;
auto bench = [](auto f) {
auto t0 = chrono::high_resolution_clock::now();
for (int i = 0; i < 50'000'000; ++i)
sink = f(i, i);
auto t1 = chrono::high_resolution_clock::now();
return chrono::duration<double>(t1 - t0).count();
};
cout << "inline: " << bench(add_inline) << "s\n";
cout << "normal: " << bench(add_normal) << "s\n";
}
Note: On optimized builds, both may be equally fast because the compiler inlines regardless of the keyword—or not at all—depending on heuristics. That’s normal with Inline in C++.
Try it yourself
- Compile with and without LTO and compare timings and binary size.
- Mark
add_normal
asstatic
and check if the compiler still inlines at-O3
in your toolchain.
🔹 static vs inline vs static inline
static
at global scope gives internal linkage (each translation unit gets its own copy). inline
preserves external linkage but makes multiple definitions ODR-safe across TUs (they must be identical). Combining static inline
still has internal linkage and is rarely needed; prefer plain inline
for header utilities.
// In a header:
// 1) static: every TU gets a private copy (no linker conflict, code bloat possible)
static int helper_static(int x) { return x + 1; }
// 2) inline: one ODR-sanctioned definition shared across TUs (linker merges)
inline int helper_inline(int x) { return x + 1; }
Guideline: Use inline
for header-defined free functions you want to share across TUs; use static
only when you explicitly want per-TU internal linkage.
Try it yourself
- Create a header with both versions and include it in multiple .cpp files. Use a debugger or map file to inspect symbol duplication.
- Measure binary size difference between many
static
helpers vs a singleinline
helper used across TUs.
🔹 Edge cases: virtual functions, recursion, and attributes
- Virtual calls are generally not inlined (dynamic dispatch). Compilers may devirtualize if the dynamic type is known (e.g.,
final
classes, whole program). - Recursive functions can be inlined for shallow recursion if the compiler decides; deep recursion generally prevents inlining.
- Non-portable attributes:
[[gnu::always_inline]]
or__forceinline
(MSVC) can insist on inlining but should be used sparingly and only when measured.
struct Base {
virtual ~Base() = default;
virtual int f() const { return 1; }
};
struct D final : Base {
int f() const override { return 2; }
};
int call_via_base(const Base& b) {
// may not inline; with 'final' and LTO, compiler might devirtualize
return b.f();
}
Try it yourself
- Mark derived classes as
final
and enable LTO; inspect if the call devirtualizes (use compiler reports or godbolt). - Add
[[gnu::always_inline]]
on a tiny function and see if your compiler honors it (non-portable!).
🔹 Mini project: Header-only math kit with inline functions and variables
Build a small header-only library that demonstrates Inline in C++ for both functions and variables.
// math_kit.hpp
#pragma once
#include <cmath>
namespace mk {
// inline variable (C++17+)
inline constexpr double kPi = 3.14159265358979323846;
inline double deg2rad(double deg)
{ // inline function
return deg * (kPi / 180.0);
}
inline double hypotenuse(double a, double b)
{ // inline function
return std::sqrt(a * a + b * b);
}
} // namespace mk
// main.cpp
#include <iostream>
#include "math_kit.hpp"
using namespace std;
int main() {
cout << mk::deg2rad(180) << "\n"; // ~3.14159
cout << mk::hypotenuse(3, 4) << "\n"; // 5
}
Output
3.14159
5
This pattern is ideal for small, header-only utilities: ODR-safe definitions, potential inlining, and clean distribution.
Try it yourself
- Add
inline constexpr double kTau = 2 * mk::kPi;
and use it in another .cpp file; verify single definition across TUs. - Create a templated
inline
functionlerp(T a, T b, double t)
and test withint
anddouble
.
🔹 Best practices for Inline in C++
- Use
inline
for small functions defined in headers to satisfy ODR and enable potential inlining. - Prefer
inline
variables (C++17+) for header-defined constants instead of non-inline globals. - Don’t rely on
inline
for performance; measure with-O2
/-O3
and LTO. The compiler decides. - Avoid making large, complex functions inline; it can bloat code and hurt instruction cache.
- Templates and
constexpr
functions are implicitly inline—no need to addinline
unless for clarity. - Remember the difference:
inline
is an ODR/linkage feature; “inlining” is an optimization.
Try it yourself
- Audit your headers: add
inline
to small free functions defined there to avoid potential ODR violations. - Measure binary size changes when marking many functions inline vs leaving them in .cpp files (with LTO on/off).
🔹 FAQs about Inline in C++
Q1: Does inline
force the compiler to inline calls?
No. It permits multiple definitions and hints that a function is a good candidate, but the optimizer decides whether to inline.
Q2: Are functions defined inside a class inline?
Yes, they are implicitly inline and ODR-safe when placed in headers.
Q3: Do I need inline
on templates?
No. Function templates defined in headers are implicitly inline.
Q4: What’s an inline variable?
A variable declared with inline
(C++17+) that can be defined in a header once and shared across translation units without violating the ODR.
Q5: Difference between static
and inline
for header functions?static
gives internal linkage (each TU gets its own copy). inline
gives external linkage with ODR-safe multiple definitions merged by the linker.
Q6: Can virtual functions be inlined?
Direct calls to a known final override can be inlined. Calls through a virtual interface usually aren’t unless devirtualized by the optimizer.
Q7: Is constexpr
faster than inline
?
They serve different purposes. constexpr
enables compile-time evaluation and implies inline. Performance depends on the optimizer and context.
Q8: Should I put every small function inline?
No. Prefer inline for small, frequently used header utilities. Measure; excessive inlining can increase code size and hurt performance.
Q9: Can I mark lambdas as inline?
Lambdas are unnamed objects with operator()
; defining them in headers is fine and compilers freely inline their calls as they see fit.
Q10: Does inline
change ABI?
Not directly. It affects linkage/ODR and may influence optimization, but ABI remains governed by the function’s signature and calling convention.
🔹 Wrapping up
Inline in C++ is primarily an ODR/linkage tool that enables safe header definitions for small functions and, since C++17, variables, while giving compilers the option to substitute calls for speed. Use it thoughtfully for concise header-only utilities, constants, and tiny helpers, but rely on measurement—not assumptions—for performance. Practice the “Try it yourself” prompts to build intuition and write clean, efficient code.