Skip to content

Case 32: Parameter Default Value Changes (C++)

Field Value
Verdict ๐ŸŸ  API_BREAK
Category API Break
Platforms Linux
Flags API break
Detected ChangeKinds param_default_value_changed, param_default_value_removed
Source files examples/case32_param_defaults/

Category: C++ Defaults | Verdict: ๐ŸŸ  API_BREAK (binary compatible)

Verdict and consumer impact

C++ resolves default argument values at the call site, during compilation of the caller โ€” the library's .so never sees them, it only receives whatever arguments the compiled caller actually passes. That means an already-built binary calling conn.connect() keeps passing 30 forever, regardless of what v2's header now says the default is; the mangled symbols (_ZN10Connection7connectEi, etc.) don't encode defaults at all, so binary compatibility is total. The break is source-only, and only for configure(): verbose loses its default, so any caller that recompiles against v2 while still calling configure() with zero arguments fails to compile. connect's changed default and disconnect's added default don't break anything, recompiled or not.

Old/new diff

Method v1.hpp v2.hpp Effect
connect void connect(int timeout = 30) void connect(int timeout = 60) default changed
configure void configure(bool verbose = true, int retries = 3) void configure(bool verbose, int retries = 5) verbose default removed; retries default changed
disconnect void disconnect(int code) void disconnect(int code = 0) default added (compatible)

abicheck command

g++ -shared -fPIC -g -std=c++17 v1.cpp -o libfoo_v1.so
g++ -shared -fPIC -g -std=c++17 v2.cpp -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so \
  --header old=v1.hpp --header new=v2.hpp \
  --ast-frontend clang --gcc-options "-std=c++17"

Expected abicheck finding

Verdict: API_BREAK (exit 2)

- param_default_value_removed: Parameter default removed: configure param verbose (True)

Quality issues:
- param_default_value_changed: Parameter default changed: connect param timeout
- param_default_value_changed: Parameter default changed: configure param retries

Minimum evidence

min_evidence: L2 โ€” default argument values live only in the header's AST (they're never emitted into DWARF or the symbol table), so this class of change is invisible below the header/AST evidence layer. abicheck's default AST backend is castxml; clang is a supported alternative frontend (--ast-frontend clang), used here because castxml isn't installed in this environment. min_evidence is scoped to linux in ground_truth.json because the castxml build used elsewhere doesn't always emit the default= attribute on Homebrew/macOS; header-AST default extraction is otherwise cross-platform.

Why abicheck catches it

The header AST records each parameter's default= value alongside its type. abicheck diffs the two versions' default-value lists per parameter: a default present in v1 and absent in v2 is a removal (source break โ€” existing zero-arg call sites stop compiling); a default whose value simply changes is a quality-level finding, since it doesn't break compilation, only silently changes behavior for callers that get recompiled.

Runtime failure demonstration

Severity: NONE (binary compatible)

Scenario: compile app against v1 headers, swap in the v2 .so without recompiling.

# Build old library + app
g++ -shared -fPIC -g -std=c++17 v1.cpp -o libfoo.so
g++ -g -std=c++17 app.cpp -I. -L. -lfoo -Wl,-rpath,. -o app
./app
# โ†’ Calling connect() with default timeout:
# โ†’   Compiled as connect(30) from v1 header
# โ†’   OK โ€” v2 default is 60, but caller already passed 30
# โ†’ ...
# โ†’ Binary is 100% compatible: same mangled symbols, same calling convention
# โ†’ NO_CHANGE at binary ABI level

# Swap in new library (no recompile)
g++ -shared -fPIC -g -std=c++17 v2.cpp -o libfoo.so
./app
# โ†’ identical output โ€” the caller's defaults were baked in at compile
#   time and never touch the library

Why no runtime failure: defaults are a pure caller-side compile-time concept in C++; there is nothing for the v2 library to get wrong at runtime, since it only ever sees the explicit integer/bool arguments the already-compiled caller passes.

Source break verification (recompiling a zero-arg configure() call against v2 fails):

cat > /tmp/source_break.cpp << 'SRC'
#include "v2.hpp"
int main() {
    Connection conn;
    conn.configure();  // no args โ€” v2 requires explicit 'verbose'
    return 0;
}
SRC
g++ -g -std=c++17 -I. /tmp/source_break.cpp -L. -lfoo -Wl,-rpath,. -o /tmp/app_v2
# โ†’ error: no matching function for call to 'Connection::configure()'
# โ†’ note: candidate: 'void Connection::configure(bool, int)'
# โ†’ note:   candidate expects 2 arguments, 0 provided
rm -f /tmp/source_break.cpp /tmp/app_v2

Safe redesign

No fix is needed for binary compatibility โ€” it's already preserved. For source compatibility: don't remove a default from a public header in a minor/patch release; if a default value must change, document it clearly, since existing compiled binaries keep silently using the old default until they're recompiled. Consider overloaded functions instead of defaults for parameters where the old-vs-new behavioral difference actually matters.

Cross-tool comparison

abidw --out-file v1.xml libfoo_v1.so
abidw --out-file v2.xml libfoo_v2.so
abidiff v1.xml v2.xml
echo "exit: $?"   # โ†’ 0 (no binary ABI change; abidiff doesn't read header
                   #     default-argument AST at all)

References


Source files

  • CMakeLists.txt
  • app.cpp
  • v1.cpp
  • v1.hpp
  • v2.cpp
  • v2.hpp

See also: Examples overview ยท All API_BREAK cases ยท Category: API Break.