Case 114: char8_t Migration (C++20 char-family โ char8_t)¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux |
| Flags | ABI break |
Detected ChangeKinds |
char8t_migration |
| Source files | examples/case114_char8t_migration/ |
Category: Binary ABI break / C++20 | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
v1 exposes a UTF-8 API over plain const char*. v2 migrates to C++20
char8_t on both function parameters and the Utf8View::data field.
char8_t is a distinct type (Itanium mangling code Du), not an alias for
char/unsigned char, so it participates in name mangling: utf8_length
mangles to _Z11utf8_lengthPKc in v1 but _Z11utf8_lengthPKDu in v2 โ a
different exported symbol. A consumer built against v1 fails to resolve
utf8_length/utf8_make against v2 without recompilation.
Old/new diff¶
| v1.cpp | v2.cpp |
|---|---|
std::size_t utf8_length(const char* text) |
std::size_t utf8_length(const char8_t* text) |
Utf8View { const char *data; ... } |
Utf8View { const char8_t *data; ... } |
abicheck command¶
g++ -std=c++20 -shared -fPIC -g v1.cpp -o libv1.so
g++ -std=c++20 -shared -fPIC -g v2.cpp -o libv2.so
abicheck compare libv1.so libv2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- char8t_migration: char8_t migration (char-family -> char8_t) on parameter 'text'
of 'utf8_make': const char * -> const char8_t *
> char8_t is a distinct C++20 type that changes overload identity and
name mangling; old binaries fail to resolve it.
- char8t_migration: char8_t migration on parameter 'text' of 'utf8_length':
const char * -> const char8_t *
- char8t_migration: char8_t migration on field 'data' of 'Utf8View':
const char * -> const char8_t *
- type_field_type_changed: Field type changed: Utf8View::data (const char * -> const char8_t *)
- func_removed: Public function removed: utf8_make
- func_removed: Public function removed: utf8_length
(The real run also surfaces build-environment noise โ a GLIBC_2.4 runtime
floor and libstdc++ symbols leaking in via std::char_traits<char8_t> โ
unrelated to the char8_t migration itself; grouped separately as
Environment & Toolchain Drift by abicheck.)
Minimum evidence¶
min_evidence: L1 โ DWARF alone carries each parameter/field's distinct
char8_t type (DW_AT_type pointing at a base type named char8_t, mangled
differently from char), so -g is enough; no public headers are needed
(this case is intentionally self-contained, taking its snapshot from the
compiled library's DWARF, not headers).
Why abicheck catches it¶
char8t_migration fires whenever a public parameter, return, or field
spelling moves between a char-family type and C++20 char8_t in either
direction โ DWARF exposes char8_t as its own distinct base type (not a
typedef/alias for char), so abicheck's type-diff sees a genuine type
change and the specialised detector names the C++20 root cause directly,
alongside the underlying func_removed/type_field_type_changed findings
that a mangled-name change and a retyped field always produce.
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: the shipped app.cpp is a minimal stub (this case is
header-free by design, so there's no shared declaration for both library
versions to build a full app against) โ the demo below uses a small
standalone consumer that declares and calls the v1 signature directly,
compiled against v1, then run against v2 without recompiling.
# Build old library + a v1-signature consumer
g++ -std=c++20 -shared -fPIC -g v1.cpp -o libv1.so
g++ -std=c++20 -g runtime_app.cpp -L. -lv1 -Wl,-rpath,. -o app
./app
# โ len=5 view.size=5
# Swap in new library (no recompile)
g++ -std=c++20 -shared -fPIC -g v2.cpp -o libv1.so
./app
# โ ./app: symbol lookup error: ./app: undefined symbol: _Z11utf8_lengthPKc
Why CRITICAL: the old mangled symbol _Z11utf8_lengthPKc is gone from
v2's dynamic symbol table (replaced by _Z11utf8_lengthPKDu); the runtime
linker cannot resolve utf8_length and the process fails to start.
Safe redesign¶
Introduce the char8_t-based API alongside the existing char-based one
under a new name (e.g. utf8_length_u8), and deprecate the old one for at
least one release cycle before removing it โ the same pattern as any other
breaking signature change (see
case02_param_type_change).
Real-world example: libraries migrating their UTF-8 surface to C++20
char8_t (a well-known source of downstream breakage after GCC/Clang
enabled -fchar8_t by default under -std=c++20) hit exactly this
mangled-name churn; nlohmann/json and fmtlib both document explicit
char8_t opt-in/opt-out handling for this reason.
Cross-tool comparison¶
Source files¶
CMakeLists.txtapp.cppv1.cppv2.cpp
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.