C++ / FUNCTIONS
Inline functions and what inline really promises
Decide when a C++ function needs inline, predict its effect on linkage and shared statics, and stop treating the keyword as a speed switch.
What you will learn
- Mark header-defined free functions inline so several .cpp files still link
- Predict one shared address and one shared static local per inline function
- Use static or an unnamed namespace when you want a private per-TU copy
- Stop using inline for speed: expansion is the optimizer's call, not the keyword's
Understanding Inline functions and what inline really promises
The name of the keyword is a leftover from 1980s compilers. What inline normatively does in modern C++ is relax the one-definition rule: an inline function may be defined in every translation unit that uses it, all those definitions must be identical, and the program then behaves as if there is exactly one function. Whether the compiler pastes the body into a call site is a separate decision made by the optimizer per call, and it is free to expand a function you never marked and to refuse to expand one you did.
That ODR permission is exactly what headers need. An ordinary function definition in a header becomes a duplicate symbol the moment two .cpp files include it and the linker rejects the program, while marking the definition inline (or writing it inside a class body, which is implicitly inline) tells the linker to fold the identical copies into one. Folded means one of everything: one address and one static local, shared by every caller in the program. Compare static or an unnamed namespace, which give internal linkage instead: each translation unit gets its own private function with its own address and its own statics.
The working mental model is that inline is a linker instruction, so decide it from where the definition lives, not from how fast you want the call to be. Actual expansion needs the body visible where the call is compiled, which is why header-defined and therefore inline-marked functions get expanded so often; the visibility did that, not the keyword. At -O0 nothing is expanded even when marked, and link-time optimization expands across .cpp files with no keyword at all. In exchange, inline demands that every translation unit calling the function also see its definition, so an inline declaration in a header with the body hidden in one .cpp is ill-formed.
<iostream>
inline int nextId() {
static int id = 100; // one object in the whole program, not one per call site
return id++;
}
inline int depth(int n) { // marked inline yet recursive: total expansion is impossible
return n == 0 ? 0 : 1 + depth(n - 1);
}
int main() {
int (*viaPointer)() = &nextId; // an inline function still has a real address
std::cout << nextId() << '\n';
std::cout << viaPointer() << '\n';
std::cout << std::boolalpha << (viaPointer == &nextId) << '\n';
std::cout << depth(4) << '\n';
}
inline is a promise to the linker that every translation unit may carry an identical definition of one shared function, not a promise that calls get expanded.
Worked examples
Implicitly inline member functions
Shows which member function definitions already carry the inline promise and which need the keyword spelled out.
<iostream>
struct Vec2 {
double x, y;
double lengthSq() const { return x * x + y * y; } // implicitly inline
double sum() const; // only declared here
};
inline double Vec2::sum() const { return x + y; } // inline is mandatory in a header
int main() {
Vec2 v{3.0, 4.0};
std::cout << v.lengthSq() << '\n';
std::cout << v.sum() << '\n';
double (Vec2::*p)() const = &Vec2::lengthSq;
std::cout << (v.*p)() << '\n';
}
Example explained
Line 1lengthSq is defined inside the class body, so it is implicitly inline and a header holding Vec2 can be included by any number of .cpp files.
Line 2Vec2::sum is defined outside the class, which makes it an ordinary definition; drop the inline there and a shared header yields duplicate symbols at link time.
Line 3&Vec2::lengthSq forces the compiler to emit a real out-of-line body for an implicitly inline function, and the call through the member pointer prints the same 25.
inline variables carry the same promise
Demonstrates that C++17 extended the ODR relaxation from functions to variables, with the same one-entity guarantee.
<iostream>
inline int gCalls = 0; // C++17: definable in a header, still one object
inline int bump() { return ++gCalls; }
int main() {
bump();
bump();
std::cout << gCalls << '\n';
int* p = &gCalls; // the variable has storage and one unique address
*p += 10;
std::cout << gCalls << '\n';
std::cout << (p == &gCalls ? "one object" : "copies") << '\n';
}
Example explained
Line 1inline int gCalls = 0; is a definition, not a declaration, yet the linker keeps a single copy however many translation units see the line.
Line 2bump needs no static local, because the shared entity here is the variable itself and every caller in the program increments the same one.
Line 3&gCalls shows that inline never means "no storage" or "no code"; it only means "fold the identical copies".
Line 4Compile with -std=c++17 or later; earlier standards reject inline on a variable.
Important notes
Member functions defined inside the class body, and every constexpr function, are already implicitly inline, so writing inline on them adds nothing.
If two translation units see different bodies for the same inline function, for example under different #ifdef settings, that is an ODR violation: the linker keeps one body arbitrarily and no diagnostic is required.
Common mistakes
Adding inline to a large function hoping for speed: at -O0 nothing is expanded at all, at -O2 the compiler expands what it likes regardless, so the only real effects are a fatter header and longer rebuilds.
Writing inline int f(); in a header and putting the body in one .cpp: the other translation units call an inline function they never saw defined, which is ill-formed and usually shows up as an undefined reference to f at link time.
Expecting the static local of an inline function to be one counter per .cpp: it is one counter for the entire program, and swapping inline for static to "keep it local" silently gives each .cpp its own counter and different numbers.
Try it yourself
Change, predict, then run
In one file write inline int hits() with a static counter, call it once directly and once through a function pointer, and print both results plus whether the pointer equals &hits. Rebuild at -O0 and at -O2 and confirm the printed numbers do not change.
Open the C++ workspaceCheck your understanding
A header defines inline int seq() { static int n = 0; return ++n; }. Two .cpp files include that header and each calls seq() exactly once. What does the second call return?
- 1, because each translation unit compiles its own copy of the body and so gets its own n
- 1 or 2, depending on whether the optimizer expanded the call at each site
- 2, because all the definitions denote a single function with a single static n
- Nothing: two translation units defining the same function is a duplicate-definition link error
Show answer
inline lets each translation unit hold an identical definition, but all of them refer to one entity, so there is exactly one n and the second call returns 2. Option 1 is tempting because it ties identity to expansion, yet an optimizer decision can never change program semantics: the static stays shared even when the body is pasted into the caller. Option 0 describes what static int seq() would do (internal linkage, one private copy per .cpp), and option 3 describes the error that inline exists to prevent.