Case 19: Enum Member Removed¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break, API break |
Detected ChangeKinds |
enum_member_removed |
| Source files | examples/case19_enum_member_removed/ |
Category: Breaking | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
Any code compiled against old that stores, transmits, or switches on
FOO's value (2) now runs against a library whose enum no longer defines
that value. This is an ABI break (a symbol using the enum was recompiled
around the change) and an API break (FOO is gone from the header). The
value 2 doesn't stop existing at the bit level, but it's no longer a valid
Status member โ persisted data, protocol messages, or switch statements
built around it silently mishandle it.
Old/new diff¶
| old/lib.h | new/lib.h |
|---|---|
enum Status { OK = 0, ERROR = 1, FOO = 2 }; |
enum Status { OK = 0, ERROR = 1 }; |
old/lib.c: get_status() returns FOO |
new/lib.c: get_status() returns ERROR |
abicheck command¶
gcc -shared -fPIC -g old/lib.c -Iold -o libfoo_v1.so
gcc -shared -fPIC -g new/lib.c -Inew -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- enum_member_removed: Enum member removed: Status::FOO (2)
> Old code uses a constant that no longer exists; compile error for
source, stale value for binaries.
Minimum evidence¶
min_evidence: L1 โ DWARF's enumeration-type debug info
(DW_TAG_enumeration_type / DW_TAG_enumerator) records each named member
and its value for both versions, so -g alone (no public headers) is enough
to detect the removed member.
Why abicheck catches it¶
DWARF records every enumerator name and constant value under the enum's
DW_TAG_enumeration_type entry; abicheck diffs the two versions' member
sets directly from debug info and flags a name present only in old as
enum_member_removed.
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: compile app against old (checks for FOO), swap in new .so
without recompile.
# Build old library + app
gcc -shared -fPIC -g old/lib.c -Iold -o libstatus.so
gcc -g app.c -Iold -L. -lstatus -Wl,-rpath,. -o app
./app
# โ FOO
# โ exit: 0
# Swap in new library (no recompile)
gcc -shared -fPIC -g new/lib.c -Inew -o libstatus.so
./app
# โ WRONG RESULT: expected FOO(2), got 1
# โ exit: 1
Why CRITICAL: the app's compiled-in check if (s == FOO) still compares
against the constant 2, but new's get_status() now returns ERROR (1)
for the situation it used to report as FOO โ the app silently falls into
its mismatch branch instead of the case it was built to handle. Any consumer
that persisted or transmitted the value 2 under the old meaning faces the
same kind of silent misinterpretation.
Safe redesign¶
Never remove a released enum member; deprecate it instead and keep its
numeric value reserved. Map legacy values deliberately in
deserialization/protocol handling, and use an explicit sentinel
(STATUS_MAX) to define the valid range rather than relying on "whatever
values currently exist."
Real-world example: protocol and status-code enums that get "cleaned up" between releases are a recurring source of this โ any peer or persisted record built against the old value set silently misinterprets removed codes.
References¶
Source files¶
CMakeLists.txtapp.c
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.