C++ Part I · Module 24 · C++ Build and Project Architecture
Name Mangling and C++ Linkage
A linker matches symbols by name, and C++ lets two functions share a name. Those two facts cannot both be true of the same name, so the compiler encodes the signature into the symbol — the namespace, the class, the parameter types, the const qualifier, the template arguments — and the result is the unreadable string you see in a link error. That encoding is usually taught as a curiosity, and it is better understood as the only type checking that survives into the link step. Everything the compiler knew about a function has been thrown away by the time two object files meet, except what the mangled name preserved, and the chapter measures what that is. The payoff is practical in three ways. A cryptic undefined-symbol error becomes readable with one command. A whole class of build failure — a header and a source file that disagree about a signature — turns out to be the mangling doing its job. And extern C becomes a consequence rather than an incantation: it is the switch that turns the encoding off, which is exactly why it enables C interoperability and exactly why it forbids overloading.
Advanced19 min readC++Part IFundamentalsBuild
Part I · Module 24 · Chapter 24.5 · Name Mangling and C++ Linkage
1. The Problem Mangling Solves
C has one function per name, so a C compiler can emit the name as written. C++ has overloading, namespaces, classes, templates and const-qualified member functions, all of which let two distinct functions share a spelling:
int freeFn(int);
int freeFn(double); // same name, different function
namespace vm { namespace bus {
class Axi {
int read(std::uint32_t);
int read(std::uint32_t, int); // same name again
int read(const Reg&) const; // and again, const-qualified
};
}}A linker matching by name needs those to be five different names. So the compiler builds a symbol from everything that distinguishes them, which is what mangling is. The encoding is defined by the Itanium C++ ABI on this platform — and by a different, incompatible scheme on MSVC, which is why object files do not move between the two.
2. What the Encoding Carries
One translation unit, nm -g --defined-only for the defined symbols, each piped through c++filt:
__Z6freeFni -> freeFn(int)
__Z6freeFnd -> freeFn(double)
__ZN2vm3bus3Axi4readEj -> vm::bus::Axi::read(unsigned int)
__ZN2vm3bus3Axi4readEji -> vm::bus::Axi::read(unsigned int, int)
__ZNK2vm3bus3Axi4readERKNS0_3RegE -> vm::bus::Axi::read(vm::bus::Reg const&) const
__ZN2vm3bus3Axi5writeEjj -> vm::bus::Axi::write(unsigned int, unsigned int)
__ZN2vm3bus3Axi7versionEv -> vm::bus::Axi::version()
__ZN2vm3bus3Axi4peekIiEET_j -> int vm::bus::Axi::peek<int>(unsigned int)
__ZN2vm3bus3Axi4peekIdEET_j -> double vm::bus::Axi::peek<double>(unsigned int)
_c_style -> c_styleThe scheme is readable once you know five pieces:
_Zintroduces a mangled name. The extra leading underscore is the Mach-O convention on Apple platforms.N … Ewraps a nested name — namespaces and classes — with each component as a length followed by the characters:2vm,3bus,3Axi.- Parameter types follow as single letters:
iisint,jisunsigned int,disdouble,vis "no parameters",Ris a reference,Kbefore a type isconst. Kimmediately after_ZNmeans the member function is const — compare__ZN…4readEjwith__ZNK…4readERKNS0_3RegE.S0_is a back-reference to an earlier component, which is how the encoding stays compact;NS0_3RegEis "Reginside the component I already named".
Three observations matter more than the syntax.
Overloads differ only in the parameter suffix. __Z6freeFni and __Z6freeFnd share everything up to the last character. That is overloading implemented: same source name, different symbol.
The const qualifier is part of the name. So a const and a non-const overload are genuinely different functions to the linker, and removing const from a member function in a header changes its symbol — which is why that edit produces an undefined-symbol error rather than silently working.
The template instantiations carry their return type and the ordinary functions do not. __ZN2vm3bus3Axi4peekIiEET_j demangles to int vm::bus::Axi::peek<int>(unsigned int) — note the leading int — while __ZN2vm3bus3Axi4readEj demangles to vm::bus::Axi::read(unsigned int) with no return type. The T_ in the template's encoding is the return type referring back to the template parameter. §6 is why that asymmetry exists and what it explains.
3. Reading a Link Error
This is the chapter's most immediately useful skill. A header and a source file that disagree:
// sig.h — what callers compile against
int configure(int cycles);
// sig.cpp — the definition drifted
long configure(long cycles) { return cycles; }Undefined symbols for architecture arm64:
"configure(int)", referenced from:
_main in sigmain.o
ld: symbol(s) not found for architecture arm64
clang++: error: linker command failed with exit code 1 (use -v to see invocation)Clang demangles for you in the message, which is a relatively recent kindness. The underlying symbols are:
_Z9configurei -> configure(int)
_Z9configurel -> configure(long)The caller wants _Z9configurei and the object file defines _Z9configurel. They differ in the final character. Nothing in the program is wrong except that two files disagree about one parameter type — and the mangling is what turned that disagreement into an error instead of a crash.
When a toolchain does not demangle, or when you are looking at a library rather than an error, two commands do it:
nm -gu myapp.o | c++filt # undefined symbols, demangled
nm -g --defined-only libvm.a | c++filt
echo '_Z9configurei' | c++filt # one symbolThe realistic causes of an undefined-symbol error, in rough order of frequency:
| the symbol that is missing | the cause |
|---|---|
| a function with a slightly wrong parameter type | the header and the source disagree — §3 |
f<int>(…) with angle brackets | a template defined in a .cpp — Chapter 24.2 §2 |
| a member function | the .cpp was not compiled, or not linked |
vtable for X or typeinfo for X | a virtual destructor or a virtual function was declared and never defined |
| a function you can see in a library | the library came before the object that needs it on the link line, or §4's internal linkage |
a const-less or const-ful variant | const was added or removed on one side only |
The vtable for X row is worth recognising on sight: it almost always means a class declared a virtual function — frequently a virtual destructor — and no translation unit provided a body.
4. Linkage, and What Hides a Symbol
Mangling decides a symbol's name. Linkage decides whether it has one at all.
External linkage — the default for functions and non-const namespace-scope variables — means the symbol is visible to the linker and can be referenced from another translation unit.
Internal linkage — static at namespace scope, anything in an anonymous namespace, and const/constexpr variables at namespace scope — means the entity is private to its translation unit. Chapter 24.1 §5 measured what that looks like:
… (__DATA,__bss) non-external __ZL13staticCounternon-external, and the mangled name carries an L after the _Z to mark the internal linkage. A symbol like that cannot be referenced from elsewhere, which is the point.
No linkage applies to local variables and function parameters: no symbol, nothing to link.
The modern way to say "private to this file" is an anonymous namespace, which is preferred over static for two reasons — it works for types as well as functions and variables, and the entities still have unique external-looking names so debuggers and profilers can distinguish them:
// in a .cpp file
namespace {
int helper(int x) { return x * 2; } // private to this TU
struct Local { int v; }; // static cannot do this
}Chapter 24.1 §5 also measured the third case, which is the one that makes headers work:
… (__DATA,__data) weak external _inlineCounterweak external is how inline is implemented: every translation unit emits a definition, all marked weak, and the linker keeps one. That is the ODR exception made concrete, and Chapter 24.2 §3 is why you must write the keyword.
5. extern "C", and Exactly What It Switches Off
extern "C" int c_style(int a, int b) { return a + b; }_c_style -> c_styleNo encoding at all. The symbol is the name, with the platform's leading underscore and nothing else — no namespace, no parameter types, no return type.
That is precisely what makes C interoperability possible: a C compiler emits _c_style, so a C++ function declared extern "C" matches it. Everything about extern "C" follows from the symbol being the bare name:
You cannot overload an extern "C" function. Two of them would produce the same symbol, so it is a compile error rather than a link error — the compiler rejects it immediately.
Name lookup, overload resolution and type checking are unaffected. extern "C" changes the symbol and the calling convention, not the language rules. Inside C++ the function is still type-checked at every call site in that translation unit.
And the type checking that crosses the seam disappears. This is the measurement:
// csig.h
extern "C" int c_configure(int cycles);
// csig.cpp
extern "C" long c_configure(long cycles) { return cycles; } exit status: 5It linked, it ran, and it returned 5. The same drift that C++ caught as Undefined symbols: "configure(int)" in §3 is, behind extern "C", a program that builds cleanly and calls a function through a signature that does not match its definition. On this platform and with these types it happens to produce the right answer; with a 32-bit caller and a 64-bit definition, or a float against an int, or a different calling convention, it produces garbage or a crash, and nothing in the build tells you.
So the honest summary is that extern "C" is a necessary tool with a real cost: it is the one place in C++ where a declaration and a definition can disagree and still link. The defence is to have exactly one declaration, in a header, that both sides include — which for a C boundary means a header carefully written to be valid in both languages:
#ifndef VM_API_H
#define VM_API_H
#ifdef __cplusplus
extern "C" {
#endif
int vm_configure(int cycles); /* one declaration, both languages */
#ifdef __cplusplus
}
#endif
#endifChapter 28 is the DPI-C application of all of this; the thing to carry forward from here is that extern "C" is a decision about the symbol, and the lost type checking is the price.
6. Why You Cannot Overload on Return Type
§5's callout gives the mechanism; the rule is worth stating plainly because it is a common interview question.
int value();
double value(); // error: functions that differ only in return type cannot be overloadedThe ordinary-function encoding has no return type, so these two would both be _Z5valuev. The language rejects it at the point of declaration rather than leaving the linker to find a duplicate symbol, which is the better diagnostic. Templates are the exception, and the measured encoding is the reason: their instantiations do carry the return type, so two instantiations differing only there are distinguishable.
The related case that is allowed, and is sometimes confused with it, is overloading on the const-ness of the object:
class Bus {
int read(Addr) const; // __ZNK… — K after _ZN
int read(Addr); // __ZN… — no K
};Those are different symbols because the const qualifier is encoded, which §2 measured directly.
7. Stability, and Why ABI Breaks Are Real
A mangled name is part of your library's binary interface. Anything that changes it breaks a caller compiled against the old header, without a recompile:
| the change | the symbol |
|---|---|
| rename a function | changes |
| change a parameter type | changes |
| add a parameter, even with a default | changes — defaults are a source-level feature |
add or remove const on a member function | changes |
| move it into or out of a namespace | changes |
| reorder two parameters of different types | changes |
| rename a parameter | unchanged |
| change the return type of an ordinary function | unchanged — and this is the dangerous one |
| change the body | unchanged |
| add a private data member | unchanged — and Chapter 24.1 §4's violation |
Two rows are the traps. A default argument does not help: configure(int) and configure(int, int = 0) are different symbols, so adding a defaulted parameter is an ABI break even though every source-level call still compiles. And changing the return type of an ordinary function leaves the symbol identical, so an old caller links against the new definition and interprets the result as the old type — a silent failure with the same shape as Chapter 24.1 §4.
That last row is why a shared library's interface is harder to evolve than its implementation, and Chapter 24.6 §7 is where that distinction decides whether you can ship a drop-in replacement.
8. The Table
| the question | the answer | measured |
|---|---|---|
| why is the symbol not my function name | overloading needs distinct names | §1 |
| what is encoded | namespace, class, parameter types, const, template args | §2 |
| is the return type encoded | no for ordinary functions, yes for template instantiations | §2 |
| so can I overload on return type | no — same symbol | §6 |
| how do I read a symbol | c++filt, or nm -gu … | c++filt | §3 |
| what does a header/source signature drift give | Undefined symbols: "configure(int)" | §3 |
what does static do to the symbol | non-external, _ZL-mangled | §4 |
what does inline do | weak external | §4 |
what does extern "C" encode | nothing — _c_style | §5 |
| so what does the same drift do there | links and runs, exit status 5 | §5 |
can I overload an extern "C" function | no — a compile error | §5 |
| does a default argument preserve the symbol | no | §7 |
| does changing an ordinary return type | yes — and that is the dangerous one | §7 |
9. What to Actually Do
- Pipe link errors through
c++filtwhen your toolchain does not demangle, and read the symbol rather than guessing. - Treat an undefined-symbol error as a disagreement between two files, and compare
nm -guon the consumer withnm -g --defined-onlyon the provider. - Recognise
vtable for Xas a declared-but-undefined virtual function, usually a destructor. - Put a C-callable function's declaration in one header wrapped in
extern "C"under#ifdef __cplusplus, and have both languages include it. - Remember
extern "C"is the one place a declaration and a definition can disagree and still link. Measured: exit status 5. - Use an anonymous namespace rather than
staticfor file-private entities — it covers types and keeps names distinguishable in tools. - Treat any signature change as an ABI break, including adding a defaulted parameter.
- Never change an ordinary function's return type in a published header without changing its name; the symbol does not move.
- Order the link line so libraries come after the objects that need them, which removes one whole class of undefined-symbol error.
10. Mistakes Worth Making Once
Guessing at an undefined symbol instead of demangling it. One command answers it.
Changing a parameter type in the source and not the header. The error names the symbol the caller wanted, which is the clue.
Adding a defaulted parameter to a shipped library function. Every existing binary caller now fails to find its symbol.
Changing an ordinary function's return type in a shipped header. The symbol is unchanged and old callers misread the result.
Declaring two extern "C" overloads. A compile error — which is at least immediate.
Relying on a extern "C" header being right. Measured: a mismatched signature links and runs.
Expecting nm to show a static function. It is non-external by design.
Forgetting const on one side of a member function declaration. A different symbol, so an undefined-symbol error.
Putting a library before its consumers on the link line. Chapter 24.6 §3 is why that matters for archives specifically.
11. Interview Relevance
The frozen curriculum marks this chapter Critical, and it is a favourite because it tests whether you understand the build model rather than a syntax rule.
"What is name mangling and why does C++ need it?" The strong answer is one sentence of mechanism and one of consequence: the linker matches by name and C++ allows two functions to share a name, so the signature is encoded into the symbol — and that encoding is the last type check in the build, which is why a header/source signature drift is a link error rather than a crash.
"Why can't you overload on return type?" Because the return type is not in the mangled name for ordinary functions, so the two would be the same symbol. The detail that makes this answer stand out is the exception: template instantiations do encode the return type, which is measurable with nm and c++filt, and that is why the rule is about ordinary functions.
"What does extern "C" do?" It turns the encoding off, which is what makes the symbol match a C compiler's — and therefore also what forbids overloading and what removes the cross-translation-unit type checking. Being able to say that a mismatched extern "C" signature links and runs puts a measured cost on the feature.
12. What You Should Be Able to Say Now
- Why mangling exists: the linker matches names, C++ permits shared names, so the signature goes into the symbol.
- How to read the encoding —
_Z,N…Efor nesting, single-letter parameter types,Kfor a const member function,S0_back-references. - That ordinary functions encode no return type and template instantiations do, and that this is exactly why overloading on return type is forbidden for the former and coherent for the latter.
- That the
constqualifier is part of a member function's symbol. - How to demangle:
c++filt,nm -gu … | c++filt. - The six common causes of an undefined symbol, including
vtable for X. - That
static/anonymous-namespace entities arenon-externalwith an_ZLname, andinlineentities areweak external. - That
extern "C"encodes nothing, which enables C interop, forbids overloading, and removes the link-time type check — measured, a drifted signature links and runs with exit status 5. - That adding a defaulted parameter breaks the ABI and changing an ordinary return type does not change the symbol.
- The unifying rule: a disagreement between translation units is detectable exactly when the disagreed-about thing survives into the symbol table — which explains Chapter 24.1 §4's silent layout bug and
extern "C"'s silent signature bug as the same phenomenon.
13. Exercises
Exercise 1 — the table. Reproduce §2 on your own class with overloads, a const member, a namespace and two template instantiations. Confirm which demangled forms show a return type.
Exercise 2 — the drift. Create §3's mismatch and read the error. Then run nm -gu on the caller and nm -g --defined-only on the definition, demangle both, and find the one differing character.
Exercise 3 — extern "C". Repeat Exercise 2 behind extern "C" and confirm it links and runs. Then make the mismatch worse — int against double — and record what the program returns.
Exercise 4 — linkage. Put a function in an anonymous namespace and another static, and compare nm -m output for both. Then try to call each from another translation unit.
Exercise 5 — the ABI. Build a dylib, link a program against it, then change the library's function to take a defaulted second parameter and rebuild only the library. Run the old program. Then repeat with a changed return type and explain the different outcome.
14. Summary
- Mangling exists because the linker matches names and C++ lets functions share one. The symbol encodes namespace, class, parameter types,
constqualification and template arguments, per the Itanium ABI on this platform. - Measured, one translation unit's symbols demangle to nine distinct functions, with
__Z6freeFni/__Z6freeFnddiffering only in the final character and__ZNK…marking the const member function. - Ordinary functions encode no return type; template instantiations do —
__ZN2vm3bus3Axi4peekIiEET_jdemangles toint …peek<int>(unsigned int). That asymmetry is why overloading on return type is forbidden for ordinary functions and coherent for templates. - Mangling is the last type check in the build. A header saying
int configure(int)against a definition oflong configure(long)givesUndefined symbols: "configure(int)"— the caller wanted_Z9configurei, the object defined_Z9configurel. extern "C"encodes nothing —_c_style— which is what makes C interop work, what forbids overloading, and what removes that check. Measured: the identical drift behindextern "C"linked, ran and returned exit status 5.- Linkage decides whether there is a symbol at all:
non-externalwith an_ZLname forstatic/anonymous-namespace entities,weak externalforinlineones, nothing for locals. Prefer an anonymous namespace tostatic. - ABI: a defaulted parameter changes the symbol (a break), renaming a parameter does not, and changing an ordinary function's return type does not — so an old caller silently misreads the result.
- The unifying rule: a cross-translation-unit disagreement is caught exactly when the disputed property survives into the symbol table. Signatures do, so they are caught. Class layouts do not, which is Chapter 24.1 §4.
extern "C"discards them on purpose, which is §5.
Chapter 24.6 takes these symbols and asks how they are packaged — and finds a library form in which a translation unit nobody references is silently dropped, along with the global constructor it contained.
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.
