C++ Name Mangling: Reading _Z Symbols, extern "C", and Link Errors

Key takeaways

C++ lets many functions share one name, but the linker only sees flat strings, so the compiler encodes namespace, class, parameter types, const and template arguments into each symbol. Reading those symbols with nm and c++filt turns most undefined reference errors into a quick diff between what the caller expected and what was defined. extern "C" turns mangling off for C interop.

Why the linker needs mangled names

A linker does not know anything about C++. Each object file contains a table of symbols, each symbol is just a string, and the linker’s job is to match every undefined string in one file with a defined string in another. In C that is enough, because a function name is unique in the whole program: print is print.

C++ breaks that assumption in several ways at once. Overloads share a name (func(int) and func(double)), the same name can appear in different namespaces and classes (A::f and B::f), a member function can come in const and non-const versions, and one template produces a separate function for every set of template arguments. All of these must end up as distinct strings in the symbol table. Name mangling is the compiler’s encoding of that extra information into the symbol name.

Here is what GCC produces for a small file, listed with nm and decoded with c++filt:

#include <string>

void func(int) {}
void func(double) {}
void func(const std::string&) {}

namespace MyLib { void func() {} int count = 0; }
int counter = 0;

class MyClass {
public:
    void get();
    void get() const;
    static int n;
};
void MyClass::get() {}
void MyClass::get() const {}
int MyClass::n = 0;

template <typename T> T twice(T x) { return x + x; }
template int twice<int>(int);  // explicit instantiation so it gets emitted

extern "C" void c_api(int) {}
_Z4funci                          func(int)
_Z4funcd                          func(double)
_Z4funcRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
                                  func(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
_ZN5MyLib4funcEv                  MyLib::func()
_ZN5MyLib5countE                  MyLib::count
counter                           counter
_ZN7MyClass3getEv                 MyClass::get()
_ZNK7MyClass3getEv                MyClass::get() const
_ZN7MyClass1nE                    MyClass::n
_Z5twiceIiET_S0_                  int twice<int>(int)
c_api                             c_api

Reading an Itanium-mangled name

GCC and Clang on Linux, macOS and MinGW follow the mangling scheme of the Itanium C++ ABI. You rarely need to decode names by hand, but knowing the main pieces makes the output of nm much less opaque:

  • _Z starts every mangled name. A symbol without it (like counter or c_api) is either a C-linkage name or a global variable in the global namespace, which Itanium does not mangle.
  • A name is written as its length followed by its characters: 4func, 5MyLib, 7MyClass.
  • N ... E wraps a nested (qualified) name: _ZN5MyLib4funcEv is MyLib::func. A K right after N marks a const member function, which is the only difference between _ZN7MyClass3getEv and _ZNK7MyClass3getEv.
  • After the name come the parameter types: i for int, d for double, v for “no parameters” (void), R for a reference, K for const, P for a pointer. RK... is therefore const ...&.
  • St is shorthand for std::, Sa for std::allocator, and S0_ and friends refer back to components that already appeared, which keeps long names from getting even longer.
  • I ... E holds template arguments: twice with IiE is twice<int>. For template instantiations the return type is also encoded (T_ here, meaning “the first template parameter”).

Notice what is not in a normal function’s name: the return type. int f(int) and double f(int) would mangle identically, which is one reason C++ does not allow overloading on return type alone.

MSVC uses a completely different scheme. void func(int) becomes something like ?func@@YAXH@Z, and its linker errors print both the decorated name and a readable form. Objects built with MSVC and objects built with GCC or Clang in GNU mode cannot be linked together, and the different mangling is the most visible sign of that. The layout of classes, exception handling and RTTI differ too, so identical mangling would not make them compatible.

Tools for reading symbols

nm program.o                 # raw symbols
nm -C program.o              # demangled (GNU nm and llvm-nm)
nm program.o | c++filt       # same, via the demangler
echo _Z4funci | c++filt      # func(int)
objdump -t program.o | c++filt

On Windows with MSVC, dumpbin /symbols lists the decorated names and undname decodes them. nm -C is what I reach for first: it lists symbols with their type letter (T for a defined function in the text section, U for undefined, D/B for data), so “is this function actually defined in this library, and with what signature?” is one grep away.

Most undefined reference errors come down to “the caller asked for one string and the definition produced another”. The linker prints the demangled version of what the caller wanted, and that is the clue.

A signature that does not match

// main.cpp
void f(long);
int main() { f(1); }

// f.cpp
void f(int) {}
undefined reference to `f(long)'

Both files compile. The declaration says long, the definition says int, and because the parameter types are part of the symbol, _Z1fl and _Z1fi are simply different names. nm -C f.o shows f(int), and the difference is obvious once you put the two side by side. The same pattern catches a missing const on a member function, a declaration in the wrong namespace, or a parameter of std::string versus const std::string&.

A C function called from C++

/* lib.c, compiled with gcc */
void c_function(void) {}
// main.cpp, compiled with g++
void c_function();
int main() { c_function(); }
undefined reference to `c_function()'

The parentheses are the giveaway. The linker looked for the C++ symbol _Z10c_functionv, printed demangled as c_function(), but the C compiler emitted plain c_function. When an undefined reference to what you know is a C function shows a parameter list, the declaration is missing C linkage.

The case I have spent the most time on is the standard library’s dual ABI in libstdc++. A library compiled with _GLIBCXX_USE_CXX11_ABI=0 (or by an old compiler) exports load(std::string const&) using the old std::string, while the application uses the new one, which lives in the inline namespace std::__cxx11. The error mentions std::__cxx11::basic_string and the library “clearly” has a load function, so it looks impossible. Comparing the mangled names shows the __cxx11 component on one side only. The fix is to build both sides with the same setting, not to add declarations.

extern “C”

extern "C" gives a declaration C language linkage: the compiler uses the C naming convention for it (on these platforms, the bare name) and the C calling convention.

extern "C" void c_function();  // symbol: c_function

The usual way to write a header that both C and C++ can include:

// mylib.h
#ifdef __cplusplus
extern "C" {
#endif

void my_function(int x);

#ifdef __cplusplus
}
#endif
// mylib.cpp
#include "mylib.h"
#include <cstdio>

void my_function(int x) {   // gets C linkage from the declaration in mylib.h
    std::printf("%d\n", x);
}

The implementation can use any C++ internally; only the entry points have to be C-compatible. The same technique is how plugin systems export factory functions, because dlsym(handle, "create_plugin") or GetProcAddress looks up a plain string. Without extern "C" you would have to pass the mangled name, which ties the plugin interface to one compiler.

A few rules and traps:

  • No overloading. With no type information in the name, two C-linkage functions with the same name collide. GCC reports error: conflicting declaration of C function 'void func(double)' with a note pointing at the previous void func(int). Give them distinct names (func_int, func_double).
  • Namespaces do not change the symbol. An extern "C" function declared inside namespace ns still has the plain symbol name, so two such functions in different namespaces are the same function to the linker.
  • Only C-compatible types across the boundary. extern "C" changes the name, not the types. Passing a std::string or throwing an exception through a C-linkage function that C code calls will not work. Use pointers, integers and plain structs at the boundary, and catch every exception before returning.
  • Do not wrap standard headers. Writing extern "C" { #include <stdio.h> } is unnecessary, because the C standard library headers already declare their functions with C linkage when compiled as C++, and it breaks if the header contains templates or other C++ declarations, as many system headers do. Wrap only your own C headers, and preferably put the guard inside the header, as shown above.

Templates and mangling

Every template instantiation is its own symbol, which has a direct consequence for where template definitions go. If twice<T> is defined in twice.cpp and only declared in a header, twice.cpp only instantiates the versions it uses itself. Another file calling twice<double> produces undefined reference to 'double twice<double>(double)': the linker knows exactly which instantiation is missing because it is spelled out in the mangled name. The fix is to put the definition in the header, or to add explicit instantiations (template double twice<double>(double);) in the .cpp for each type you support.