Skip to content

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:

  1. Overloading: void print(int) and void print(float) share the same name.
  2. Namespaces: std::sort and boost::sort share the same name.
  3. Templates: vector<int> and vector<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).

Proof: Overloading Requires Encoding Beyond the Name

Section titled “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.

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``_ZTI for RTTI).
  • Typeinfo structures for typeid support.
  • 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).

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.

A mangled name generally follows this pattern: _Z + [N for Nested] + [Name Length][Name]... + [E for End] + [Argument Types]

Consider the function:

namespace net {
class Socket {
void connect(int port, bool ssl);
};
}

Mangled Output: _ZN3net6Socket7connectEib

  1. _Z: Prefix marking a mangled symbol.
  2. N: Start of nested scope (Namespace/Class).
  3. 3net: Identifier “net” (3 characters).
  4. 6Socket: Identifier “Socket” (6 characters).
  5. 7connect: Identifier “connect” (7 characters).
  6. E: End of nested scope.
  7. i: First argument type int.
  8. b: Second argument type bool.

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&lt;std::string&gt; 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&lt;std::__cxx11::basic_string&lt;char, ...&gt;&gt;.

Reading and Parsing Mangled Names (Itanium)

Section titled “Reading and Parsing Mangled Names (Itanium)”

The Itanium ABI defines a complete grammar for mangled names. Key type encodings:

Source TypeMangledNotes
voidv
boolb
inti
unsigned intj
long longx
floatf
doubled
char const*PKcP = pointer, K = const
std::stringNSt6basic_stringIcSt11char_traitsIcESaIcEEEFull qualified name
int&RiR = lvalue reference
int&&OiO = rvalue reference
int const&RKi
... (variadic)z
template&lt;int N&gt;IiEI starts template args, E ends

Parsing Example: std::vector<int>::push_back(int const&)

Section titled “Parsing Example: std::vector<int>::push_back(int const&)”

Step by step through _ZNSt6vectorIiSaIiEE9push_backERKi:

  1. _Z — mangled symbol prefix
  2. N — begin nested name
  3. Ststd:: namespace (special encoding)
  4. 6vector — “vector” (6 chars)
  5. IiSaIiEE — template args: int``std::allocator&lt;int&gt;
  6. 9push_back — “push_back” (9 chars)
  7. E — end nested name
  8. RKiint const& (return type not encoded for non-template functions)

Result: std::vector&lt;int, std::allocator&lt;int&gt;&gt;::push_back(int const&)

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_NAMEE where T_NAME is 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 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&lt;int&gt;(...) 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.

Terminal window
# View the longest mangled names in a binary
nm -C ./app | awk '{ print length, $0 }' | sort -rn | head -20

:::note Return Types The Itanium ABI generally does not encode the return type of a function, as C++ does not support overloading based solely on return type. Exception: Template functions and auto return type deduction may trigger return type encoding. :::

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

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

  1. ?: Start of mangled symbol.
  2. connect: Function name.
  3. @: Delimiter.
  4. Socket@net: Scope (reversed order: Class, then Namespace).
  5. @@: End of scope list.
  6. QEAA: Access qualifiers (public/private) and calling convention (e.g., __cdecl``__stdcall).
  7. X: Return type (void). Note: MSVC encodes return types.
  8. H: First argument int.
  9. _N: Second argument bool.
  10. @Z: End of signature.

Key MSVC Mangling Differences from Itanium

Section titled “Key MSVC Mangling Differences from Itanium”
AspectItanium (GCC/Clang)MSVC
Prefix_Z?
Return typeNot encoded ()Always encoded
Access controlNot encodedEncoded (public, private, protected)
Calling conv.Not encodedEncoded (A = cdecl, G = stdcall)
Scope orderOutermost to innermostInnermost to outermost (reversed)
Namespace stdSt?A or std@@
SubstitutionsS_``S0_Etc.Not used (names encoded in full)

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_func on some systems).
  • Restriction: extern "C" functions cannot be overloaded or templated.

Per [N4950 S10.5], an extern "C" linkage specification causes the entity to use C language Linkage. The practical effect on mangling:

  1. 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).
  2. Variables: Similarly unmangled.
  3. 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

Section titled “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

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);

:::caution Mixing function pointers with different linkage is undefined behavior [N4950 S10.5 p7]. A Function pointer of type extern "C" and one of type C++ linkage are different types, even if they Have the same parameter and return types. Passing one where the other is expected is UB. :::

You rarely decode mangled names manually. Use the toolchain utilities to translate them.

c++filt is part of GNU Binutils (or LLVM). It accepts a mangled name and prints the C++ signature.

Terminal window
$ c++filt _ZN3net6Socket7connectEib
net::Socket::connect(int, bool)

llvm-cxxfilt is the LLVM equivalent. It supports the same Itanium mangling and can also demangle Rust symbols:

Terminal window
$ llvm-cxxfilt _ZN3net6Socket7connectEib
net::Socket::connect(int, bool)
# Pipe nm output through c++filt for readable output
$ nm ./app | c++filt

The nm tool accepts a --demangle (or -C) flag.

Terminal window
$ nm -C ./app
0000000000001149 T net::Socket::connect(int, bool)

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.

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.

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.

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();.

Given the linker error:

undefined reference to `_ZNK5Utils8parseArgERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE'`

Step-by-step demangling:

  1. _Z — mangled prefix
  2. N — nested name
  3. K — const qualifier (this function is const-qualified)
  4. 5Utils — namespace Utils
  5. 8parseArg — function parseArg
  6. E — end nested name
  7. R — reference
  8. K — const
  9. NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEconst std::__cxx11::string&

Result: Utils::parseArg(std::__cxx11::basic_string&lt;char, std::char_traits&lt;char&gt;, std::allocator&lt;char&gt;&gt; const&amp;) 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.

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;
}

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)"
}

Heavy template usage results in massive symbol names. A std::map&lt;std::string, std::vector&lt;MyCustomClass&gt;&gt; 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 template prevents 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

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.

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)

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 .strtab size.
  • MSVC COFF: Internal limits may cause truncation for names exceeding ~4096 characters.
  • nm output: Line wrapping may make long names hard to read. Use nm -C for demangled output.
  • 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 without extern "C" causes linker errors or undefined behavior at runtime.
  • Mismatched const in signatures: void foo(int) and void 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::__cxx11 symbol mismatches. Always recompile everything with the same compiler.
  • Trusting type_info::name(): The output is implementation-defined. Use abi::__cxa_demangle for 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).

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
}

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.

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

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.

:::caution In C++17, noexcept became part of the type system. This means function pointer types Are different if their noexcept specification differs. A void(*)(int) and a void(*)(int) noexcept are different types and cannot be implicitly converted.

When encountering an obscure linker error involving a mangled name, follow this systematic workflow:

  1. Copy the mangled name from the linker error output.
  2. Demangle it using c++filt or llvm-cxxfilt:
    Terminal window
    echo '_ZNK5Utils8parseArgERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE' | c++filt
  3. Identify the namespace, class, and function from the demangled name.
  4. Search the source code for the function declaration. Check:
  • Is the function declared in the TU that references it?
  • Is the correct extern "C" linkage specified (if applicable)?
  • Are the parameter types an exact match (including const``&``&&)?
  • Is the noexcept specification consistent?
  1. Check the library/object file that should provide the symbol:
    Terminal window
    nm -C libutils.a | grep "Utils::parseArg"
  2. If the symbol exists but the linker cannot find it, check link order (libraries after object files) and visibility (-fvisibility=hidden).

14. Mangling Reference: Complete Type Encoding Table

Section titled “14. Mangling Reference: Complete Type Encoding Table”

The following table provides a comprehensive reference for Itanium ABI type encodings:

Source TypeMangledNotes
voidv
boolb
charc
signed chara
unsigned charh
shorts
unsigned shortt
inti
unsigned intj
longl
unsigned longm
long longx
unsigned long longy
__int128n
floatf
doubled
long doublee
float _ComplexCfC99 complex
double _ComplexCdC99 complex
T*PTP = pointer
T&RTR = lvalue reference
T&&OTO = rvalue reference
T constKTK = const
T volatileVTV = volatile
T const volatileVK
T[]AT_Array of unknown bound
T[N]AT_NArray of N elements

This table covers the most common encodings. The full specification is in the Itanium C++ ABI Document, section 5.1.5.

15. Mangling and Structured Bindings (C++17)

Section titled “15. Mangling and Structured Bindings (C++17)”

Structured bindings [N4950 S9.7] do not introduce new mangled names. The compiler decomposes the Structured binding into individual references to the underlying object’s members or tuple elements. These references are local to the scope and do not appear in the symbol table.

Coroutine-related symbols are mangled with special prefixes:

  • _ZSt16coro_resume_pvstd::coro_resume(void*)
  • _ZSt17coro_destroy_pvstd::coro_destroy(void*)
  • _ZSt12coro_done_pvstd::coro_done(void*)

The coroutine frame itself is a compiler-generated struct with a mangled name based on the coroutine Function’s signature. The frame type is internal to the compiler and does not appear in the public Symbol table.

This topic covers the essential concepts and techniques related to name mangling, including key principles and practical applications.

Key concepts include:

  • core concepts and definitions
  • key principles and frameworks
  • practical applications
  • common techniques and methods
  • evaluation and critical analysis

A thorough understanding of these concepts, combined with regular practice and review, is essential for mastery of this topic.

Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.

:::