Name Mangling
Linkers were originally designed for C and Fortran, languages where function names are unique Identifiers. C++, however, introduces features that break this assumption:
- Overloading:
void print(int)andvoid print(float)share the same name. - Namespaces:
std::sortandboost::sortshare the same name. - Templates:
vector<int>andvector<float>generate distinct code for the same class template.
To enforce uniqueness in the global symbol table, C++ compilers perform Name Mangling (also Called Decoration). They encode the function signature (Scope, Name, Arguments) into a strict ASCII String.
Understanding mangling is required to decipher linker errors (“Undefined reference to _ZN3foo3barEi”) and to design interoperability interfaces (FFI).
1. Why Name Mangling Is Necessary
Proof: Overloading Requires Encoding Beyond the Name
Consider two functions in C++:
void process(int x);
void process(double x);
The linker operates on a flat symbol table where each entry is a (name, address) pair. Both Functions are named process. Without additional encoding, the linker cannot distinguish them.
More formally, the C++ language permits function overloading [N4950 S8.4.1]: two functions in the Same scope may have the same name if their parameter-type-lists are different. Since the linker’s Symbol table is a global flat map keyed by string (the symbol name), the compiler must encode the Parameter types into the name to produce unique keys.
The encoding must be deterministic (the same function always produces the same mangled name) and injective (two functions with different signatures produce different mangled names). This is Guaranteed by the ABI specification for each platform.
What Gets Mangled
Per the Itanium C++ ABI, the following entities are mangled:
- Top-level functions (including member functions, static member functions).
- Global variables with external or module linkage.
- Template instantiations (both function and class templates).
- Virtual table entries (
_ZTV``_ZTIfor RTTI). - Typeinfo structures for
typeidsupport. - Guard variables for static local initialization (
_ZGV).
Local variables, static variables with internal linkage, and entities in unnamed namespaces are not mangled (they do not appear in the symbol table).
2. The Itanium C++ ABI (GCC / Clang)
The Itanium C++ ABI is the standard mangling scheme used by GCC, Clang, and the majority of the Unix ecosystem (Linux, macOS, BSD, Android). It is designed to be parseable and logical.
Structure
A mangled name generally follows this pattern: _Z + [N for Nested] + [Name Length][Name]... + [E for End] + [Argument Types]
Encoding Logic
Consider the function:
namespace net \{
class Socket \{
void connect(int port, bool ssl);
\};
\}
Mangled Output: _ZN3net6Socket7connectEib
_Z: Prefix marking a mangled symbol.N: Start of nested scope (Namespace/Class).3net: Identifier “net” (3 characters).6Socket: Identifier “Socket” (6 characters).7connect: Identifier “connect” (7 characters).E: End of nested scope.i: First argument typeint.b: Second argument typebool.
Substitution Compression
A critical optimization in the Itanium ABI is substitution. When a name appears multiple times In a mangled symbol, subsequent occurrences are replaced with a reference (S_``S0_``S1_ Etc.). This prevents mangled names from growing exponentially for deeply nested types.
Consider:
void foo(std::vector<std::string>, std::vector<std::string>);
Without substitution, std::vector<std::string> would be encoded twice in full. With Substitution, the second occurrence is replaced by S_:
_Z3fooSt6vectorINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEESaIS5_ES5_
The S5_ refers to the sixth substitution candidate (numbered starting from 0), which is std::vector<std::__cxx11::basic_string<char, ...>>.
Reading and Parsing Mangled Names (Itanium)
The Itanium ABI defines a complete grammar for mangled names. Key type encodings:
| Source Type | Mangled | Notes |
|---|---|---|
void | v | |
bool | b | |
int | i | |
unsigned int | j | |
long long | x | |
float | f | |
double | d | |
char const* | PKc | P = pointer, K = const |
std::string | NSt6basic_stringIcSt11char_traitsIcESaIcEEE | Full qualified name |
int& | Ri | R = lvalue reference |
int&& | Oi | O = rvalue reference |
int const& | RKi | |
... (variadic) | z | |
template<int N> | IiE | I starts template args, E ends |
Parsing Example: std::vector<int>::push_back(int const&)
Step by step through _ZNSt6vectorIiSaIiEE9push_backERKi:
_Z— mangled symbol prefixN— begin nested nameSt—std::namespace (special encoding)6vector— “vector” (6 chars)IiSaIiEE— template args:int``std::allocator<int>9push_back— “push_back” (9 chars)E— end nested nameRKi—int const&(return type not encoded for non-template functions)
Result: std::vector<int, std::allocator<int>>::push_back(int const&)
Template Argument Encoding
Template arguments are encoded within I...E delimiters. Each argument is encoded according to its Type:
- Type arguments: Encoded using the normal type encoding rules.
- Non-type arguments (integers): Encoded as
LiVALUEE(literal, value, end). - Template template arguments: Encoded as
IT_NAMEEwhereT_NAMEis the template name.
template<typename T, int N>
void process(T array[N]);
// process<int, 10> mangles the template args as:
// IiLi10EE (int, literal 10)
Template Instantiation Mangling
Template instantiations produce some of the longest mangled names. Each template argument is fully Encoded, recursively:
template<typename T>
void process(std::map<std::string, std::vector<T>>);
An instantiation like process<int>(...) produces a deeply nested mangling because std::map And std::vector each carry their full template argument lists. For complex types, mangled names Can exceed 1000 characters.
## View the longest mangled names in a binary
nm -C ./app | awk '\{ print length, $0 \}' | sort -rn | head -20
3. The Microsoft ABI (MSVC)
The Microsoft Visual C++ compiler uses a proprietary mangling scheme. It is significantly more Verbose and includes information not found in Itanium (such as Access Control levels and Calling Conventions).
Structure
The pattern generally begins with ? and relies on special characters (@``$) as delimiters.
Consider the same function:
namespace net \{
class Socket \{
void connect(int port, bool ssl);
\};
\}
Mangled Output (Approximate): ?connect@Socket@net@@QEAAXH_N@Z
?: Start of mangled symbol.connect: Function name.@: Delimiter.Socket@net: Scope (reversed order: Class, then Namespace).@@: End of scope list.QEAA: Access qualifiers (public/private) and calling convention (e.g.,__cdecl``__stdcall).X: Return type (void). Note: MSVC encodes return types.H: First argumentint._N: Second argumentbool.@Z: End of signature.
Key MSVC Mangling Differences from Itanium
| Aspect | Itanium (GCC/Clang) | MSVC |
|---|---|---|
| Prefix | _Z | ? |
| Return type | Not encoded () | Always encoded |
| Access control | Not encoded | Encoded (public, private, protected) |
| Calling conv. | Not encoded | Encoded (A = cdecl, G = stdcall) |
| Scope order | Outermost to innermost | Innermost to outermost (reversed) |
| Namespace std | St | ?A or std@@ |
| Substitutions | S_``S0_Etc. | Not used (names encoded in full) |
4. Disabling Mangling (extern "C")
To allow C code, Rust, Python (ctypes), or Java (JNI) to call a C++ function, you must disable Mangling. This forces the linker to use the “C” representation (the bare function name).
extern "C" void my_api_func(int x);
- Mangled:
my_api_func(or_my_api_funcon some systems). - Restriction:
extern "C"functions cannot be overloaded or templated.
How extern "C" Affects Mangling
Per [N4950 S10.5], an extern "C" linkage specification causes the entity to use C language Linkage. The practical effect on mangling:
- Functions: The function name is used without encoding parameter types. The symbol in the object file is the function name (with a possible platform-specific prefix like
_on Windows). - Variables: Similarly unmangled.
- No overloading: Since the mangled name does not encode types, two
extern "C"functions with the same name but different parameters would produce the same symbol, which the linker would flag as a duplicate definition.
Architectural Best Practice: The API Boundary
When designing a shared library (.dll/.so) for general consumption, wrap the interface in extern "C" block to provide a stable ABI.
#ifdef __cplusplus
extern "C" \{
#endif
MYLIB_API void initialize_system();
MYLIB_API int process_data(void* ptr);
#ifdef __cplusplus
\}
#endif
extern "C" as a Mangling Escape Hatch
A common pattern is to use extern "C" as an opaque function pointer type when interfacing with C APIs that require callbacks:
// C library expects a function pointer with C linkage
extern "C" typedef void (*CallbackFunc)(int);
// C++ function with C linkage for the callback
extern "C" void my_callback(int value) \{
// Can use C++ features inside
std::vector<int> v;
v.push_back(value);
\}
// Register with C API
c_library_register_callback(my_callback);
5. Inspection and Demangling Tools
You rarely decode mangled names manually. Use the toolchain utilities to translate them.
Demangling with c++filt
c++filt is part of GNU Binutils (or LLVM). It accepts a mangled name and prints the C++ signature.
$ c++filt _ZN3net6Socket7connectEib
net::Socket::connect(int, bool)
Demangling with llvm-cxxfilt
llvm-cxxfilt is the LLVM equivalent. It supports the same Itanium mangling and can also demangle Rust symbols:
$ llvm-cxxfilt _ZN3net6Socket7connectEib
net::Socket::connect(int, bool)
## Pipe nm output through c++filt for readable output
$ nm ./app | c++filt
Inspecting Binaries (nm)
The nm tool accepts a --demangle (or -C) flag.
$ nm -C ./app
0000000000001149 T net::Socket::connect(int, bool)
Demangling with undname
Visual Studio provides undname.exe.
> undname ?connect@Socket@net@@QEAAXH_N@Z
void __cdecl net::Socket::connect(int,bool)
Inspecting Binaries (dumpbin)
dumpbin requires explicit flags to show symbols.
> dumpbin /SYMBOLS app.obj
Note: dumpbin output is often raw; piping it to a text file for analysis is recommended.
6. Name Mangling and Linker Errors
When the linker reports Undefined reference to ...Look closely at the mangled name (or the Demangled output). The mangling encodes the exact signature, making it possible to diagnose Mismatches.
Mismatched Signatures
If the linker reports:
undefined reference to `net::Socket::connect(int, bool)'
But your code declares:
void connect(int port); // Missing bool parameter
The mangled names will differ (_ZN3net6Socket7connectEib vs _ZN3net6Socket7connectEi), and the Linker will correctly report the missing symbol.
Dual-ABI Mismatch (GCC)
GCC 5 introduced a new ABI for std::string (using std::__cxx11::basic_string). If you link code Compiled with GCC 4 against a library compiled with GCC 5+, the mangled names for std::string Parameters will differ:
undefined reference to `foo(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)'
Fix: Recompile all code with the same compiler version, or define _GLIBCXX_USE_CXX11_ABI=0 to Use the old ABI.
Missing extern "C"
If a C library exports init but your C++ code declares extern void init(); without extern "C" The compiler mangles the reference:
undefined reference to `_Z4initv'
But the library exports the unmangled symbol init. The fix is extern "C" void init();.
Decode Example: Step-by-Step
Given the linker error:
undefined reference to `_ZNK5Utils8parseArgERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE'`
Step-by-step demangling:
_Z— mangled prefixN— nested nameK— const qualifier (this function is const-qualified)5Utils— namespaceUtils8parseArg— functionparseArgE— end nested nameR— referenceK— constNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE—const std::__cxx11::string&
Result: Utils::parseArg(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&) const
The K after N indicates this is a const member function. If your declaration lacks constThe Mangled name will differ, causing the linker error.
7. RTTI and type_info Name Mangling
RTTI (typeid``dynamic_cast) relies on mangled names internally. The std::type_info::name() Member function returns a compiler-specific, mangled representation of the type name:
#include <iostream>
#include <typeinfo>
int main() \{
std::cout << typeid(int).name() << "\n"; // "i" (GCC/Clang)
std::cout << typeid(std::string).name() << "\n"; // "NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE"
return 0;
\}
Important: The output of name() is not guaranteed to be human-readable or portable. For a Demangled name, use abi::__cxa_demangle (GCC/Clang) or typeid(T).name() with c++filt:
#include <iostream>
#include <typeinfo>
#include <cxxabi.h>
#include <cstdlib>
std::string demangle(const char* mangled) \{
int status = 0;
char* demangled = abi::__cxa_demangle(mangled, nullptr, nullptr, &status);
std::string result(demangled ? demangled : mangled);
std::free(demangled);
return result;
\}
int main() \{
std::cout << demangle(typeid(std::string).name()) << "\n";
// Output: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >
return 0;
\}
Mangling in Debug Builds
In debug builds (-g``-O0), the compiler emits additional DWARF debug information (.debug_info Sections) that contains the demangled names. However, the symbol table still contains mangled names. The DWARF info allows debuggers (GDB, LLDB) to map mangled symbols back to their source-level names.
Some compilers emit a __func__ or __PRETTY_FUNCTION__ string literal that contains the unmangled Signature. This is independent of the mangling scheme and is available even in release builds:
void foo(int x) \{
std::cout << __PRETTY_FUNCTION__ << "\n";
// Output: "void foo(int)"
\}
8. Architectural Implications
Binary Size (Templates)
Heavy template usage results in massive symbol names. A std::map<std::string, std::vector<MyCustomClass>> might result in a mangled string That is 400+ characters long.
- Impact: This bloats the string table of the binary, increasing executable size on disk.
- Mitigation:
extern templateprevents instantiating the same symbols in every object file.
// In header:
extern template class std::vector<MyClass>;
// In one .cpp file:
template class std::vector<MyClass>; // Explicit instantiation
ABI Incompatibility
Because Itanium and MSVC use fundamentally different encoding logic, it is impossible to link an Object file compiled with Clang (GNU CLI) on Windows against a library compiled with MSVC, unless Using C-linkage (extern "C"). Clang-CL exists specifically to mimic the MSVC mangling scheme to Allow compatibility.
DSO Symbol Visibility
On ELF platforms, symbols are exported from shared libraries by default. This means all mangled C++ Symbols are visible in the dynamic symbol table, increasing binary size and load time. Use -fvisibility=hidden and explicit __attribute__((visibility("default"))) to control which symbols Are exported:
# CMake: hide all symbols by default
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)
Name Length Limits
The Itanium ABI does not impose a hard limit on mangled name length. However, some toolchain Components may have practical limits:
- ELF symbol table: No hard limit, but extremely long names increase
.strtabsize. - MSVC COFF: Internal limits may cause truncation for names exceeding ~4096 characters.
nmoutput: Line wrapping may make long names hard to read. Usenm -Cfor demangled output.
Common Pitfalls
- Forgetting
extern "C"for C callbacks: C++ function pointers have different mangling than C function pointers. Passing a C++ function to a C API callback withoutextern "C"causes linker errors or undefined behavior at runtime. - Mismatched
constin signatures:void foo(int)andvoid foo(const int)produce different mangled names in some contexts (member functions), causing unexpected undefined references. - Dual-ABI mismatch: Mixing GCC 4.x and GCC 5+ object files causes
std::__cxx11symbol mismatches. Always recompile everything with the same compiler. - Trusting
type_info::name(): The output is implementation-defined. Useabi::__cxa_demanglefor portable, human-readable type names. - Assuming ABI compatibility between Itanium and MSVC: Object files compiled with different ABI schemes cannot be linked. Use
extern "C"for cross-compiler interop. - Inline functions in headers without
inline: Defining a non-inline function in a header included by multiple TUs causes duplicate symbol errors because each TU produces a mangled symbol with the same name (but in different COMDAT groups if the linker supports it).
9. Mangling and Inline Namespaces
Inline namespaces [N4950 S9.8.2] affect mangling because they contribute to the qualified name. An Inline namespace is encoded as part of the nested name:
namespace V1 \{
inline namespace Detail \{
struct Config \{ int value; \};
\}
\}
The mangled name for V1::Config is _ZN2V16ConfigE (the inline namespace Detail does not appear In the mangled name for most lookup contexts). However, the full mangled name for V1::Detail::Config would include Detail. The exact behavior depends on whether the lookup Resolves through the inline namespace or directly.
Inline namespaces are commonly used for versioning ABI-compatible changes in library headers:
namespace lib \{
inline namespace v2 \{
void process(int x);
\}
// v1::process(int) still accessible as lib::process(int) for backward compatibility
\}
10. Mangling and Lambdas
Lambda closures produce unnamed types with compiler-generated names. These names are mangled Uniquely using a counter based on the lexical scope:
auto f = [](int x) \{ return x * 2; \};
The closure type is mangled as something like _ZZ1fvENK3$_0clEi (lambda in function f0th Lambda, operator() taking int). The exact encoding is compiler-specific but follows the general Pattern of including the enclosing function name and a lambda index.
This is relevant when debugging: if a lambda appears in a linker error (unlikely but possible if the Lambda captures something that requires a global symbol), the mangled name will include the Compiler-generated type name, which is unreadable without demangling.
11. Mangling and Concepts (C++20)
Concepts [N4950 S7.5] do not produce mangled names in the symbol table because they are compile-time Constraints, not runtime entities. However, concept names appear in constraint mangling for Constrained templates.
When a template has a constrained requires clause, the constraint is not encoded in the mangled Name. Two function templates with the same name and parameters but different constraints produce the Same mangled name, which would be an ODR violation if both were defined:
template<typename T>
requires std::integral<T>
void process(T x);
template<typename T>
requires std::floating_point<T>
void process(T x); // Same mangled name as above: _Z7processIiEvT_ (for int)
This is not a practical problem because the compiler rejects the second definition as a redefinition At compile time. The standard prohibits defining two templates with the same name and parameter list But different constraints [N4950 S13.7.3 p6].
12. Mangling and noexcept Specifications
Per the Itanium ABI, noexcept is part of the function type and does affect mangling in C++17 And later. A noexcept function and a potentially-throwing function with the same name and Parameters produce different mangled names:
void foo(int x); // _Z3fooi
void bar(int x) noexcept; // _Z3barNi (note the 'N' suffix for noexcept)
This can cause subtle linker errors when a function is declared noexcept in one TU but not in Another. The mangled names differ, and the linker reports an undefined reference. The fix is to Ensure the noexcept specification is consistent across all declarations.