Case 69: Trivially Copyable to Non-Trivial (Calling Convention Change)¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS |
| Flags | ABI break |
Detected ChangeKinds |
value_abi_trait_changed |
| Source files | examples/case69_trivial_to_nontrivial/ |
Category: Calling Convention | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
~Point() {} makes Point non-trivially copyable. Under the Itanium C++
ABI (GCC/Clang on Linux), a trivial Point is passed in FP registers
(%xmm0/%xmm1); a non-trivial Point is passed via a hidden pointer
instead. The function signature and mangled symbol name are unchanged โ
this is a pure calling-convention break, not a source-level API change โ
so source recompiles cleanly but old binaries crash or read garbage,
because the caller and callee now disagree on where the arguments live.
Old/new diff¶
| v1.h | v2.h |
|---|---|
struct Point { double x; double y; }; |
struct Point { double x; double y; ~Point() {} }; |
| (trivially copyable โ passed in registers) | (non-trivially copyable โ passed via hidden pointer) |
abicheck command¶
g++ -shared -fPIC -g v1.cpp -o libfoo_v1.so -lm
g++ -shared -fPIC -g v2.cpp -o libfoo_v2.so -lm
abicheck compare libfoo_v1.so libfoo_v2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- value_abi_trait_changed: DWARF value-ABI trait changed: distance(Point, Point)
(p0:trivial|p1:trivial -> p0:nontrivial|p1:nontrivial)
Minimum evidence¶
min_evidence: L1 โ DWARF's parameter-type DIEs carry enough information
(a non-trivial special member function, i.e. the user-defined destructor)
for abicheck to reclassify Point's value-ABI trait; -g alone is enough,
no public headers required.
Why abicheck catches it¶
abicheck inspects each parameter type's DWARF representation for user-declared destructors/copy/move members that make a type non-trivially-copyable, and compares that trait between the two versions per parameter of each matched function โ independent of the (unchanged) mangled signature or struct size.
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: compile app against v1, swap in v2 .so without recompile.
# Build v1 (trivially copyable) and app
g++ -shared -fPIC -g v1.cpp -o libpoint.so -lm
g++ -g app.cpp -L. -lpoint -Wl,-rpath,. -o app
./app
# โ distance = 5.0
# โ Expected: 5.0
# Swap in v2 (non-trivially copyable, no recompile)
g++ -shared -fPIC -g v2.cpp -o libpoint.so -lm
./app
# โ Segmentation fault (core dumped)
Why CRITICAL: the mangled name is identical, so the dynamic linker
happily resolves the symbol. The app passes Point values in FP registers
(v1 convention); v2 expects hidden pointers in integer registers instead.
The callee dereferences what it thinks are pointers, causing the segfault
observed above โ with no compiler warning at either build.
Safe redesign¶
Treat the trivially-copyable property as part of the ABI contract. Never add user-defined special member functions to a type passed by value across a library boundary โ pass by pointer/reference instead, or add the destructor from day one so the ABI is established as non-trivial from the start:
/* Safe: pass by pointer, immune to trivially-copyable changes */
double distance(const Point *a, const Point *b);
Real-world example: this is a recurring theme in the C++ ABI stability
debate โ std::string/std::unique_ptr in libstdc++ have specific
trivially-copyable properties that constrain their implementation, and the
Chromium project explicitly documents which IPC types must stay trivially
copyable for exactly this reason.
Cross-tool comparison¶
References¶
- Itanium C++ ABI ยง3.1.2: Non-trivial types passed by invisible reference
- System V AMD64 ABI: Classification of aggregate types
- P2028R0: What is ABI, and What Should WG21 Do About It?
Source files¶
CMakeLists.txtapp.cppv1.cppv1.hv2.cppv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.