Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
__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_style

The scheme is readable once you know five pieces:

  • _Z introduces a mangled name. The extra leading underscore is the Mach-O convention on Apple platforms.
  • N … E wraps 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: i is int, j is unsigned int, d is double, v is "no parameters", R is a reference, K before a type is const.
  • K immediately after _ZN means the member function is const — compare __ZN…4readEj with __ZNK…4readERKNS0_3RegE.
  • S0_ is a back-reference to an earlier component, which is how the encoding stays compact; NS0_3RegE is "Reg inside 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.

This is the chapter's most immediately useful skill. A header and a source file that disagree:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// sig.h — what callers compile against
int configure(int cycles);

// sig.cpp — the definition drifted
long configure(long cycles) { return cycles; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
_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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
nm -gu myapp.o | c++filt          # undefined symbols, demangled
nm -g --defined-only libvm.a | c++filt
echo '_Z9configurei' | c++filt    # one symbol

The realistic causes of an undefined-symbol error, in rough order of frequency:

the symbol that is missingthe cause
a function with a slightly wrong parameter typethe header and the source disagree — §3
f<int>(…) with angle bracketsa template defined in a .cpp — Chapter 24.2 §2
a member functionthe .cpp was not compiled, or not linked
vtable for X or typeinfo for Xa virtual destructor or a virtual function was declared and never defined
a function you can see in a librarythe library came before the object that needs it on the link line, or §4's internal linkage
a const-less or const-ful variantconst 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
… (__DATA,__bss) non-external __ZL13staticCounter

non-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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
… (__DATA,__data) weak external _inlineCounter

weak 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
extern "C" int c_style(int a, int b) { return a + b; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
_c_style -> c_style

No 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// csig.h
extern "C" int c_configure(int cycles);

// csig.cpp
extern "C" long c_configure(long cycles) { return cycles; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  exit status: 5

It 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#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
#endif

Chapter 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int    value();
double value();      // error: functions that differ only in return type cannot be overloaded

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 changethe symbol
rename a functionchanges
change a parameter typechanges
add a parameter, even with a defaultchanges — defaults are a source-level feature
add or remove const on a member functionchanges
move it into or out of a namespacechanges
reorder two parameters of different typeschanges
rename a parameterunchanged
change the return type of an ordinary functionunchanged — and this is the dangerous one
change the bodyunchanged
add a private data memberunchanged — 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 questionthe answermeasured
why is the symbol not my function nameoverloading needs distinct names§1
what is encodednamespace, class, parameter types, const, template args§2
is the return type encodedno for ordinary functions, yes for template instantiations§2
so can I overload on return typeno — same symbol§6
how do I read a symbolc++filt, or nm -gu … | c++filt§3
what does a header/source signature drift giveUndefined symbols: "configure(int)"§3
what does static do to the symbolnon-external, _ZL-mangled§4
what does inline doweak external§4
what does extern "C" encodenothing — _c_style§5
so what does the same drift do therelinks and runs, exit status 5§5
can I overload an extern "C" functionno — a compile error§5
does a default argument preserve the symbolno§7
does changing an ordinary return typeyes — and that is the dangerous one§7

9. What to Actually Do

  • Pipe link errors through c++filt when 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 -gu on the consumer with nm -g --defined-only on the provider.
  • Recognise vtable for X as 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 static for 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…E for nesting, single-letter parameter types, K for 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 const qualifier 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 are non-external with an _ZL name, and inline entities are weak 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, const qualification and template arguments, per the Itanium ABI on this platform.
  • Measured, one translation unit's symbols demangle to nine distinct functions, with __Z6freeFni/__Z6freeFnd differing only in the final character and __ZNK… marking the const member function.
  • Ordinary functions encode no return type; template instantiations do — __ZN2vm3bus3Axi4peekIiEET_j demangles to int …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 of long configure(long) gives Undefined 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 behind extern "C" linked, ran and returned exit status 5.
  • Linkage decides whether there is a symbol at all: non-external with an _ZL name for static/anonymous-namespace entities, weak external for inline ones, nothing for locals. Prefer an anonymous namespace to static.
  • 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.