Skip to content
VLSI Mentor

C++ Part I · Module 6 · Functions

inline Functions and the One-Definition Rule

This module has created a problem it has not yet solved. Chapter 6.1 argued for putting declarations in a header so that callers and the definition are checked against each other, and every chapter since has used functions short enough that splitting them across two files would be absurd — a one-line twice, a four-line mean. The obvious thing is to put the whole definition in the header, and the obvious thing breaks, in a way Chapter 2.6's table already predicted: linking catches a name defined twice, and a definition in a header is a definition in every translation unit that includes it. An include guard does not help, which is the first thing most people try and the first thing this chapter measures. The fix is a keyword, and the keyword is the reason this chapter is worth reading rather than skimming. Its name says inline, which sounds like a request to the optimiser, and almost everyone learns it that way. The measurement says otherwise twice over: compiled without optimisation, the function marked inline was not inlined, and compiled with optimisation, a function never marked inline was. What the keyword actually changes is visible in the object file, and this chapter looks at it.

Intermediate17 min readC++Part IFundamentalsFunctions

Part I · Module 6 · Chapter 6.8 · inline Functions and the One-Definition Rule

1. The Problem This Module Created

Chapter 6.1 §7 recommended a three-file arrangement: declarations in a header, definitions in a source file, and the header included by both the definer and the callers.

For a substantial function that is right. For the functions in this module it is ridiculous:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int twice(int n) { return 2 * n; }

Splitting that across two files means the body is in a place nobody will look, the compiler cannot see it from a call site, and you have two files to open to read one line. The natural thing is to put it in the header.

The natural thing is also a link error, and §3 measures it.

2. The One-Definition Rule

C++ has a rule with a name, usually abbreviated ODR. The part of it that matters here is short:

A function may be declared as many times as you like, and defined exactly once in the whole program.

Chapter 6.1 §1 established the declaration/definition split, and Chapter 2.6 §7's table already named both halves of the enforcement. Linking catches "a name promised and never defined" — which Chapter 6.1 §5 measured — and "a name defined twice", which is this chapter.

The second half is easy to violate without noticing, because #include is textual. Chapter 2.5 established what the preprocessor does: it copies the header's text into the file. So a definition in a header is a definition in every translation unit that includes it.

3. Measured: the Duplicate Symbol

Three files. A header with a definition, and two source files that include it:

Azvya Education Pvt. Ltd.VLSI Mentor
util.h — a definition in a header, with no inline
#ifndef UTIL_H
#define UTIL_H
int twice(int n) { return 2 * n; }     // definition in a header, NOT inline
#endif
Azvya Education Pvt. Ltd.VLSI Mentor
a.cpp
#include "util.h"
int from_a(int n) { return twice(n); }
Azvya Education Pvt. Ltd.VLSI Mentor
b.cpp
#include "util.h"
#include <iostream>
int from_a(int n);
int main() { std::cout << from_a(3) << " " << twice(5) << '\n'; return 0; }

Compile each translation unit separately and both are fine:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -Wall -Wextra -Wpedantic -c a.cpp -o a.o
$ c++ -std=c++17 -Wall -Wextra -Wpedantic -c b.cpp -o b.o
$                                            ← zero errors from both

Then link them:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ a.o b.o -o prog
duplicate symbol 'twice(int)' in:
    /private/tmp/odr/a.o
    /private/tmp/odr/b.o
ld: 1 duplicate symbols
clang++: error: linker command failed with exit code 1

This is Chapter 2.6 §7's other row, met for real. Each compilation saw one definition and was satisfied; neither could see the other. The linker saw two and refused.

You can confirm it in the object files directly. nm lists a file's symbols:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ nm -g a.o | grep twice      → 1 definition
$ nm -g b.o | grep twice      → 1 definition

Two object files, two definitions, one program. That is the violation.

4. The Include Guard Does Not Help

util.h has an include guard — the #ifndef / #define / #endif wrapper from Chapter 2.5 — and the error happened anyway. This is worth being explicit about, because reaching for a guard is the first instinct and it solves a different problem.

An include guard prevents a header from being included twice in one translation unit. It does nothing about the header being included once in each of several translation units.

The preprocessor runs separately for every source file. When a.cpp is compiled, UTIL_H is not defined, so the guard lets the text through. When b.cpp is compiled — a completely separate run, with a fresh set of macros — UTIL_H is not defined either, so the guard lets the text through again.

The guard did its job both times. Its job was never the ODR.

5. inline, and What It Actually Changes

Add one keyword to the header:

Azvya Education Pvt. Ltd.VLSI Mentor
util2.h — the same definition, with inline
#ifndef UTIL2_H
#define UTIL2_H
inline int twice(int n) { return 2 * n; }     // one definition per TU is allowed
#endif

Measured: both translation units compile, the link succeeds, and the program runs:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -Wall -Wextra -Wpedantic -c a2.cpp -o a2.o
$ c++ -std=c++17 -Wall -Wextra -Wpedantic -c b2.cpp -o b2.o
$ c++ a2.o b2.o -o prog2
$ ./prog2
6 10

What inline means is this: this function may be defined in more than one translation unit, provided every definition is identical. The linker is then required to treat them as one function rather than as a collision.

The mechanism is visible in the object file. nm -m shows each symbol's attributes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ nm -m a.o  | grep twice        ← without inline
0000000000000000 (__TEXT,__text) external __Z5twicei

$ nm -m a2.o | grep twice        ← with inline
0000000000000024 (__TEXT,__text) weak external __Z5twicei

external against weak external. That one word is the whole difference. A weak definition is one the linker is allowed to see several of: it keeps one and discards the rest, instead of reporting a duplicate.

Note also that the symbol is still there. inline did not remove the function or fold it into its callers — the definition is present in the object file, with a name and an address. It is simply marked as mergeable.

Two obligations come with the keyword, and both are on you:

Every definition must be identical. The rule is that the definitions be token-for-token the same, which in practice means: one definition, in one header, included everywhere. Two headers with slightly different versions of the same inline function is undefined behaviour, and the linker will not catch it — it is entitled to pick either one.

It belongs in the header, not the source file. An inline function must be visible in every translation unit that calls it, because each one needs its own definition.

6. inline Does Not Mean Inlined

Now the part that matters most, because the keyword's name is actively misleading.

Inlining is an optimisation: replacing a call with a copy of the function's body, so there is no call at run time. The keyword's name comes from a time when it was a hint about that, and in modern C++ it is not one.

Here is a program with both kinds of function, where the values are read from input so nothing can be folded away at compile time:

Azvya Education Pvt. Ltd.VLSI Mentor
inlining.cpp — complete program
#include <iostream>

inline int twice(int n) { return 2 * n; }     // 'inline'
static int thrice(int n) { return 3 * n; }    // NOT 'inline'

int main()
{
    int a = 0;
    std::cin >> a;                 // keeps the value unknown at compile time
    std::cout << twice(a) + thrice(a) << '\n';
    return 0;
}

Compile to assembly at two optimisation levels and count how many times each function's symbol appears — a symbol reference means a real call survived:

buildtwice (marked inline)thrice (not marked)
-O0present, 4 referencespresent, 3 references
-O20 references0 references

Read both rows.

At -O0, the inline function was not inlined. There is a real function and a real call to it. The keyword bought nothing in terms of code generation.

At -O2, the function that was never marked inline was inlined. The optimiser decided, on its own, that a three-character body was not worth a call — and the keyword had no part in that decision.

inline controls linkage. Whether a call is inlined is the optimiser's decision, and the keyword is not how you ask.

7. The Other Ways to Put a Function in a Header

inline is one of several things that make a definition in a header legal, and the others are worth naming so that their absence of an inline keyword does not confuse you.

FormWhy it is allowedWhere
inline functionexplicitly mergeablehere
member function defined inside a classimplicitly inlineChapter 11.3
constexpr functionimplicitly inlineChapter 21.2
function templatetemplates have their own ODR allowanceChapter 18.3
static functioneach TU gets its own private copyChapter 7.5
inline variable (C++17)the same idea for dataChapter 24.1

The static row is a genuinely different answer rather than a variation. static gives each translation unit a separate function with internal linkage, so there is no collision because there is nothing shared. That also means several copies in the final program and several distinct addresses for what reads as one function, which matters in cases Chapter 7.5 covers. It is why every example in this module used static — Chapter 6.1 §3 flagged it — and why the fix for a header is inline rather than static.

A note on the class row, because it is the one you will meet most: a function defined inside a class body is inline without the keyword. That is the reason class definitions can live entirely in headers, which is the normal arrangement in C++ and the thing Module 11 onward depends on.

8. What to Actually Do

Put a function definition in a header only with inline (or one of §7's implicit forms).

Write the definition exactly once, in one header, and include that header everywhere.

Do not use inline to request speed. Measured: it does not, and the optimiser does not need asking.

Do not reach for an include guard to fix a duplicate symbol. It is the wrong tool — §4.

Read a duplicate-symbol error as "a definition is in a header without inline" nine times out of ten.

Use static for a function private to one source file, and inline for one shared through a header.

Keep substantial functions in a source file. The header arrangement of Chapter 6.1 §7 is still right for anything longer than a few lines; inline is for the short ones.

Use nm when a linker error does not make sense. nm -g lists definitions; nm -m shows whether they are weak.

9. Mistakes Worth Making Once

Defining a function in a header without inline. Measured: both files compile, and the link reports duplicate symbol 'twice(int)'.

Adding an include guard to fix it. The guard was already there. It prevents double inclusion in one translation unit, not one definition per translation unit.

Thinking inline is a speed request. Measured: not inlined at -O0; the unmarked function was inlined at -O2.

Thinking inline removes the function. Measured: the symbol is still in the object file, marked weak external.

Putting an inline definition in a .cpp. Callers in other translation units then have no definition to use.

Letting two headers carry different versions of one inline function. Undefined behaviour, and the linker will not tell you.

Using static in a header to dodge the error. It works and gives every translation unit its own copy with its own address — Chapter 7.5.

Reading inline in a codebase as a performance marker. It means "defined in a header".

10. Interview Relevance

This is one of the most reliable discriminators in a C++ interview, because the common answer is wrong.

"What does inline do?" The weak answer is "asks the compiler to inline the function". The strong answer is that it permits the function to be defined in multiple translation units, requiring the definitions to be identical, and that it has no guaranteed effect on whether calls are inlined — the optimiser decides that independently.

"Why does a function in a header need it?" Because #include is textual, so the definition appears in every translation unit that includes it, and the ODR allows only one definition per program. Mentioning that an include guard does not help is the detail that shows experience.

"How would you check?" nm -m on the object file: without inline the symbol is external, with it the symbol is weak external. A candidate who reaches for the object file rather than for a remembered rule is demonstrating the habit the job actually needs.

11. What You Should Be Able to Say Now

  1. The ODR: a function may be declared many times and defined exactly once in the whole program.
  2. Chapter 2.6 §7's table named both halves — "promised and never defined" (Chapter 6.1 §5) and "defined twice" (this chapter).
  3. #include is textual, so a definition in a header is a definition in every translation unit that includes it.
  4. Measured: both translation units compiled cleanly and the link failed with duplicate symbol 'twice(int)', naming both object files.
  5. An include guard does not help. It prevents double inclusion within one translation unit; the preprocessor runs fresh for each.
  6. inline means the function may be defined in several translation units, provided the definitions are identical.
  7. Measured mechanism: nm -m shows external without it and weak external with it — the linker merges weak definitions instead of rejecting them.
  8. The symbol is still present; inline does not remove or fold the function.
  9. inline does not mean inlined. Measured at -O0: the inline function kept a real call.
  10. Measured at -O2: the function that was never marked inline was inlined too.
  11. So inline controls linkage, and inlining is the optimiser's decision.
  12. Its two obligations: identical definitions, and put it in the header, not the source file.
  13. Implicitly inline: class member functions defined in the class (11.3) and constexpr functions (21.2); templates have their own allowance (18.3).
  14. static is a different answer — a private copy per translation unit, with its own address — which is why it is wrong for a shared header.

12. Exercises

Exercise 1 — which stage fails? For a non-inline function defined in a header included by two source files, say whether each stage succeeds: preprocessing, compiling each file, linking. Give the message from the one that fails.

Exercise 2 — explain the guard. A colleague replaces the #ifndef guard with #pragma once and the duplicate-symbol error persists — measured, it does. Explain in two sentences why, and give the fix.

Exercise 3 — read the symbols. You run nm -m on two object files and see external __Z4meani in one and weak external __Z4meani in the other. Say which was compiled from a header with inline and what will happen when you link them together.

Exercise 4 — predict the inlining. For a program with an inline function f and a non-inline static function g, both one-liners, say whether each call survives in the generated code at -O0 and at -O2, and name the thing that decides.

Exercise 5 — pick the keyword. For each, say inline, static, or neither: (a) a one-line helper used by three source files (b) a one-line helper used only inside one source file (c) a 200-line algorithm used by three source files (d) a function you want the compiler to inline.

Exercise 6 — the undetected bug. Two headers each define inline int scale(int) with different bodies, and both are included somewhere in the program. Say what the standard says about this, what the linker will do, and why this is worse than a duplicate-symbol error.

Exercise 7 — answer the interview question. In no more than four sentences, answer "what does inline do?" so that a reader can tell you have measured it rather than memorised it.

13. Summary

  • The ODR: declared many times, defined once per program.
  • #include is textual, so a header definition lands in every including translation unit.
  • Measured: clean compiles, then duplicate symbol 'twice(int)' at link, naming both object files.
  • An include guard does not help — it is per translation unit, and the preprocessor restarts for each.
  • inline permits several identical definitions and requires the linker to merge them.
  • Measured mechanism: external becomes weak external in nm -m; the symbol remains present.
  • inline does not mean inlined. Measured: not inlined at -O0, while the unmarked function was inlined at -O2.
  • Obligations: identical definitions, and in the header.
  • Implicitly inline: in-class member functions, constexpr functions; templates have their own rule.
  • static gives a private copy per translation unit — a different answer, not a substitute.

One chapter remains, and it is the only one in this module with nothing new to measure. Everything mechanical about functions is now established: how to declare one, what a call does with its arguments, three ways to take a parameter and three ways to hand something back, how to let a caller omit an argument, how one name can serve several types, and where a definition is allowed to live. What is left is the judgement that uses all of it — because every one of those choices is visible in a signature, and a signature is the only part of a function most of its callers will ever read.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.