Skip to content

Case 16 β€” Inline β†’ Non-inline (ODR / Symbol Appearance)

Field Value
Verdict 🟒 COMPATIBLE
Category Addition (Compatible)
Platforms Linux
Flags β€”
Detected ChangeKinds β€”
Source files browse on GitHub

Verdict: 🟒 COMPATIBLE

What changes

Version Where is fast_hash?
v1 Header-only inline β€” callers have their own copy
v2 Moved to .so β€” now an exported symbol

What breaks at binary level

Scenario A β€” Stale callers (compiled with v1 header): The caller has fast_hash inlined. The v2 .so also has fast_hash. At link time, the linker sees two definitions β€” caller's inlined copy and the .so export. Normally the inline version "wins" locally. But if the implementation diverges between v1 (inlined) and v2 (in .so), results differ. This is an ODR violation.

Scenario B β€” Fresh callers (compiled with v2 header, linked against v1 .so): The caller expects fast_hash as an imported symbol. But v1 .so has no fast_hash symbol at all. Link fails with "undefined symbol: fast_hash".

In both scenarios the breakage is subtle and depends on build order.

Why abidiff misses it

abidiff compares two .so files. v1 .so has no fast_hash symbol (it was inline). v2 .so adds fast_hash. abidiff reports this as a new export, not a breaking change. It cannot know that callers were compiled with the old inline version.

Why ABICC catches it

ABICC parses both header ASTs. It sees fast_hash was inline in v1 and non-inline in v2. This is a semantic change: the inline assumption is gone. ABICC flags:

"Function 'fast_hash' changed: inline removed".

Real-world example

In abseil-cpp, several string utility functions were moved from headers into the .so during the monorepo refactor (2021). Users who pinned to old .so files but updated their headers got linker errors. Some projects shipped both a header-inline and a .so symbol β€” causing ODR violations with LTO builds.

Code diff

-// v1.hpp
-inline int fast_hash(int x) {
-    return static_cast<int>(static_cast<unsigned>(x) * 2654435761U);
-}

+// v2.hpp
+int fast_hash(int x);   // declaration only

+// v2.cpp
+int fast_hash(int x) {  // now in .so
+    return static_cast<int>(static_cast<unsigned>(x) * 2654435761U);
+}

Reproduce steps

cd examples/case16_inline_to_non_inline

# Build .so files
g++ -shared -fPIC -std=c++17 -g v1.cpp -o libv1.so
g++ -shared -fPIC -std=c++17 -g v2.cpp -o libv2.so

# Check symbol table
nm --dynamic libv1.so | grep fast_hash || echo "v1: no fast_hash symbol (expected)"
nm --dynamic libv2.so | grep fast_hash            # v2: symbol present

# abidiff: shows fast_hash as NEW addition (not a break)
abidw --out-file v1.xml libv1.so
abidw --out-file v2.xml libv2.so
abidiff v1.xml v2.xml || true

# ABICC: catches inline→non-inline semantic change
abi-compliance-checker -lib fast_hash -v1 1.0 -v2 2.0 \
  -header v1.hpp -header v2.hpp

Real Failure Demo

Severity: CRITICAL

Scenario B β€” linker error: compile app with v2.hpp (no inline), link against v1.so (no symbol).

# Build v1.so (fast_hash is inline β€” NOT in .so)
g++ -shared -fPIC -std=c++17 -g v1.cpp -o libhash.so

# Compile app with v2.hpp (declaration only, expects symbol in .so)
g++ -std=c++17 -g app.cpp -I. -L. -lhash -Wl,-rpath,. -o app
# β†’ /usr/bin/ld: app.cpp:(.text+0x...): undefined reference to 'fast_hash(int)'
# β†’ collect2: error: ld returned 1 exit status

# Only works when linking against v2.so (has the symbol)
g++ -shared -fPIC -std=c++17 -g v2.cpp -o libhash.so
g++ -std=c++17 -g app.cpp -I. -L. -lhash -Wl,-rpath,. -o app  # links OK
./app
# β†’ fast_hash(42) = ...

Why CRITICAL: Existing binaries compiled against v1 (inline) are unaffected β€” they have the inline body baked in. The break hits new consumers: any code compiled against v2.hpp (declaration only) that links against v1.so gets a hard linker error because the symbol doesn't exist in v1.so. This forces a coordinated upgrade: v2.hpp and v2.so must ship together.

Why runtime result may differ from verdict

Inline→non-inline: old binary uses inlined copy, runtime unaffected

References


Source files

See also: Examples overview Β· All COMPATIBLE cases Β· Category: Addition (Compatible).