Name Collision / ODR Violation with Shared Libraries
The Setup
hello.cpp defines a class nt with a method print(), used internally by
print_obj(). main.cpp defines a namespace nt with a free function
print(), and calls both nt::print() and print_obj().
hello.cpp is compiled into a shared library (libhello.so on Linux,
libhello.dll on Windows), and main.cpp is linked against it.
The Problem
C++ name mangling encodes both namespaces and classes identically in the Itanium ABI (used on Linux):
namespace nt { void print() } → _ZN2nt5printEv
class nt { void print() } → _ZN2nt5printEv
Both produce the exact same mangled symbol. The compiler has no way to distinguish them at link time.
What Actually Happens
With -O0 (no optimization):
libhello.soexports_ZN2nt5printEvas a weak symbol (the class method)maindefines_ZN2nt5printEvas a strong/global symbol (the namespace function)- When
print_obj()in the shared library callsnt::print(), the dynamic linker resolves_ZN2nt5printEvat runtime and the strong symbol inmainwins - Result:
print_obj()callsmain'snt::print()instead of the class method - Output:
"Hello from namespace"printed twice
With -O3 (optimizations on):
- The compiler inlines the class
nt::print()directly intoprint_obj() _ZN2nt5printEvis never emitted as a standalone symbol inlibhello.so- No collision occurs — each call resolves correctly
- Output: correct
This is why the bug was optimization-dependent and hard to spot.
Symbol Evidence
# -O0: libhello.so exports the class method as weak
00000000000011d0 w F .text _ZN2nt5printEv ← weak, can be overridden
# -O3: libhello.so does NOT export it at all (inlined away)
# only _Z9print_objv is present
One Definition Rule (ODR)
The C++ standard (basic.def.odr) requires that any entity with more than one definition across translation units must have identical definitions. Violating this is ill-formed, no diagnostic required — the compiler and linker are not required to warn you.
The consequences are undefined behavior: the program may crash, produce wrong output, or appear to work correctly depending on compiler flags, optimization level, or link order.
Linux Dynamic Linker Behavior
On Linux, the dynamic linker (ld.so) uses a flat symbol namespace by
default. When a shared library references a symbol, the linker searches all
loaded objects (including the main executable) and resolves to the first
strong symbol found, regardless of which DSO the symbol logically belongs to.
This is different from macOS (which uses two-level namespaces by default) and Windows (where DLL symbols are explicitly imported/exported via import tables and don't collide this way).
This flat namespace behavior means a weak symbol in a .so can be silently
overridden by any strong symbol of the same name anywhere in the process — even
from the main executable.
Why Windows Is Not Affected
On Windows, this collision does not occur. The DLL symbol model is fundamentally different:
- Only symbols explicitly marked
__declspec(dllexport)are visible outside the DLL — everything else is private by default - The linker generates an import library (
.lib) with stubs that reference the DLL by name, so the loader knows exactly which DLL each symbol comes from - There is no flat global symbol namespace — each DLL is its own isolated scope
This means the internal class nt in hello.cpp was never at risk on Windows
regardless of optimization level. The bug is Linux (and ELF) specific.
To keep hello.cpp portable, the __declspec(dllexport) on print_obj is
wrapped in a macro:
#ifdef _WIN32
#define EXPORT __declspec(dllexport)
#else
#define EXPORT
#endif
On Linux EXPORT expands to nothing. On Windows it marks the symbol for export.
The Linux Fix
Wrap internal-use classes in an anonymous namespace in hello.cpp:
namespace {
class nt {
public:
void print() { ... }
};
}
This gives the class internal linkage. The mangled symbol becomes:
_ZN12_GLOBAL__N_12nt5printEv
The _GLOBAL__N_1 prefix is the ABI encoding for the anonymous namespace,
making it unique per translation unit and invisible to the dynamic linker. No
collision is possible.
The Linux and Windows fixes solve the same problem from opposite defaults:
- Linux: opt-in to hiding symbols (anonymous namespace,
-fvisibility=hidden) - Windows: opt-in to exporting symbols (
__declspec(dllexport))
Key Takeaways
- Class and namespace names mangle identically in the Itanium C++ ABI
- Weak symbols in shared libraries can be silently hijacked by strong symbols in the executable
- ODR violations are UB with no required diagnostic — bugs may only appear at certain optimization levels
- This is a Linux/ELF-specific problem — Windows DLLs use explicit export tables and are not affected
- Internal implementation details in shared libraries should use anonymous namespaces to prevent symbol leakage on Linux
- Use
objdump -tornmto inspect symbol visibility and catch these issues early