C / FUNCTIONS
Inline functions and what inline really promises
Use inline correctly in C: know it only offers a body for substitution, supply the external definition once, or use static inline instead.
What you will learn
- Write an inline definition in a header plus one extern declaration in a single .c file
- Reach for static inline when a header helper needs no shared address
- Diagnose an undefined reference caused by an inline definition with no external one
- Keep modifiable statics out of inline definitions with external linkage
Understanding Inline functions and what inline really promises
The inline keyword in C does two things that look unrelated, and only one of them is about speed. It asks the compiler to consider substituting the function body at the call site, which the compiler is free to refuse for any reason it likes: optimization is off, the address is taken, the body is recursive or simply too big. The part that actually bites is the second: in C99 and later, if every file-scope declaration of a function in a translation unit says inline and none says extern, then that translation unit's definition is an inline definition and does not provide the external definition of the function.
So any call the compiler declined to substitute becomes a call to a symbol that must be defined somewhere else in the program. If nothing defines it you get an undefined reference, typically at -O0 where nothing is inlined, in a project that linked fine at -O2. The fix is to give exactly one translation unit a file-scope declaration that is not inline-without-extern; the usual spelling is extern inline int clampi(int, int, int); in one .c file, which promotes that file's definition to the program's single external definition.
The alternative is static inline, which is why headers are full of it: internal linkage means each including file gets its own private definition, so there is never a missing symbol and never a question about which body ran, at the cost of a possible copy and a distinct address per translation unit. One more rule follows naturally from this picture: an inline definition with external linkage may not define a modifiable static object and may not reference an identifier with internal linkage, because the program can hold several inline copies plus one external copy that could not share such state. For the same reason, if a header body and an out-of-line body ever disagree, which one a given call uses is unspecified.
<stdio.h>
/* Inline definition: a candidate for substitution in this file.
By itself it does NOT provide the external definition of clampi. */
inline int clampi(int v, int lo, int hi)
{
return v < lo ? lo : (v > hi ? hi : v);
}
/* One non-inline declaration in this translation unit promotes the
definition above to the program's external definition, so the linker
has a symbol even when nothing is substituted (as at -O0). */
extern int clampi(int v, int lo, int hi);
int main(void)
{
int (*fp)(int, int, int) = clampi; /* taking an address needs a real copy */
printf("clamped: %d %d %d\n", clampi(-5, 0, 10), clampi(7, 0, 10), clampi(42, 0, 10));
printf("through pointer: %d\n", fp(99, 0, 10));
return 0;
}
In C, inline is not a speed switch: it marks a definition as available for substitution and by itself does not give the linker the external definition it may still need.
Worked examples
static inline sidesteps the linkage rule
Shows that internal linkage removes any need for an external definition, and what a static inline body is allowed to hold that an extern one is not.
<stdio.h>
/* Internal linkage: this file always has a body available, so no
external definition of next_pow2 is ever required. */
static inline unsigned next_pow2(unsigned n)
{
unsigned p = 1;
while (p < n)
p <<= 1;
return p;
}
static inline unsigned tally(unsigned n)
{
static unsigned calls = 0; /* legal because the function is static */
calls++;
return calls * n;
}
int main(void)
{
printf("%u %u %u\n", next_pow2(1), next_pow2(17), next_pow2(1000));
printf("%u ", tally(10));
printf("%u ", tally(10));
printf("%u\n", tally(10));
return 0;
}
Example explained
Line 1static inline unsigned next_pow2(...) has internal linkage, so the compiler can inline it or emit a private copy, and the linker is never asked for a shared symbol.
Line 2The while (p < n) p <<= 1; loop is an ordinary body; substitution changes only whether a call instruction is emitted, not the result.
Line 3static unsigned calls is permitted here only because the function is static; an inline definition with external linkage may not define a modifiable static object.
Line 4The output 10 20 30 shows one calls object for the whole file, however many times the body was substituted; a second .c file including the same static inline would get its own.
inline changes nothing you can observe at runtime
Demonstrates that an inline function still has exactly one address once an external definition exists, and that direct and indirect calls agree.
<stdio.h>
inline double area(double r) { return 3.14159265358979 * r * r; }
extern double area(double r); /* emit the external definition here */
typedef double (*fn)(double);
int main(void)
{
fn a = area;
fn b = &area;
printf("same address: %s\n", a == b ? "yes" : "no");
printf("direct %.4f\n", area(2.0));
printf("indirect %.4f\n", a(2.0));
return 0;
}
Example explained
Line 1extern double area(double r); is what gives area a real out-of-line body, which is required the moment you store its address.
Line 2fn a = area; and fn b = &area; are equivalent because a function name already converts to a pointer to that function, so & is redundant.
Line 3a == b prints yes: there is one function in the program, so all pointers to it compare equal even if some calls were inlined.
Line 4area(2.0) may be substituted while a(2.0) goes through the pointer, yet both print 12.5664, because inline never alters what the body computes.
Important notes
GCC's pre-C99 semantics are the mirror image: under -std=gnu89 or -fgnu89-inline, a plain inline definition also emits an external definition and extern inline means inline-only, which is why old code uses extern inline to mean the opposite of what C99 says.
An inline declaration with the definition hidden in another .c file promises nothing at all; the compiler can only substitute a body it can see, so the keyword is only useful where the definition is visible.
Common mistakes
Putting a plain inline definition in a header, calling it from several files, and never providing the external definition: the build succeeds at -O2 where every call is substituted, then fails with 'undefined reference to clampi' at -O0 or as soon as somebody takes the address.
Treating inline as a command to inline and sprinkling it on functions for speed: the compiler ignores it whenever it prefers a call, while a static inline copy in dozens of translation units grows the binary for no measurable gain.
Keeping a modifiable static counter inside a non-static inline function in a header: that violates a constraint on inline definitions, so you may get a diagnostic, and where it compiles each copy counts separately instead of sharing one value.
Try it yourself
Change, predict, then run
In one file, write inline unsigned bit_count(unsigned x) that counts set bits, call it from main on 0xF0F0, and add extern unsigned bit_count(unsigned); so it links. Then delete that extern line and see whether your compiler still links the program at its default optimization level.
Open the C workspaceCheck your understanding
A header defines inline int clampi(int v, int lo, int hi) and three .c files include it and call it. The project links cleanly at -O2 but fails with 'undefined reference to clampi' at -O0. What is the actual cause?
- At -O0 the compiler substitutes nothing, so the calls refer to an external definition that no translation unit ever emitted
- inline gives the function internal linkage, so each file's copy is invisible to the other files
- Defining a function in a header is a duplicate definition, and -O0 stops merging the copies
- The function needs a prototype before its definition, and only -O2 infers one automatically
Show answer
Because every declaration in each file says inline without extern, each file holds only an inline definition, which is not an external definition; at -O2 every call is substituted so the symbol is never referenced, while at -O0 real calls are emitted and nothing defines the symbol. Option 2 is tempting because static inline really does have internal linkage, but plain inline keeps external linkage here; the missing piece is the external definition, supplied by adding extern inline int clampi(int, int, int); in one .c file or by switching to static inline.