Skip to content

Case 105: Concept Tightening (C++20)

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

Category: Subtle source break / regression suite | Verdict: 🟒 COMPATIBLE (known gap β€” see below)

What breaks

A C++20 concept gains an additional requirement (e.g. Addable previously required only a + b; v2 additionally requires T(), default-constructibility). The mangled name of the already-shipped instantiation (sum<int>) is unchanged, so previously-compiled binaries keep linking. The break is at the consumer call site: any consumer instantiating sum<T> against a type that fails the new requirement no longer compiles against v2's header.

This is the prototypical "concept tightening" case. The change is invisible at the binary layer and invisible to all current snapshot- based detectors.

Why this is in the regression suite

Concept tightening is the C++20 evolution of the older SFINAE-narrowing pattern (std::enable_if<...>): the library author narrows the set of types a template accepts in a way that the symbol table cannot reveal. oneTBB, the standard library, and many algorithm-heavy libraries do this on purpose β€” sometimes to fix a latent bug, sometimes to nudge users toward "better" types β€” and every such tightening is a silent source-break for whoever was relying on the relaxed contract.

How abicheck catches it (and where it doesn't)

The diff currently exposes:

  • nothing on the sum<int> instantiation (mangled name unchanged, the exported symbol set is identical between v1 and v2)
  • nothing on the concept itself

This is a documented known_gap. castxml emits C++20 concept declarations as

<Unimplemented kind="Concept"/>

with no name, no body, and no link to the templates that use the concept. There is therefore no way to detect concept tightening from the castxml dump path that abicheck currently uses.

Closing the gap requires the header-AST capture path on the roadmap: a libclang-based extractor that runs alongside castxml and serializes each concept's constrained type-parameters and requires-expression body. With that in place, the diff would emit a new CONCEPT_TIGHTENED ChangeKind whenever the requires-expression gains a constraint, and the symmetric CONCEPT_RELAXED when it loses one.

This case is preserved as a regression fixture so that whenever the header-AST path lands, the detector has a concrete fixture to validate against.

Code diff

v1 v2
concept Addable = requires(T a, T b) { a + b; }; adds T() to the requirement set
sum<wrapped> compiles (wrapped has operator+) sum<wrapped> fails (no default ctor)

Real Failure Demo

Severity: KNOWN_GAP / SOURCE BREAK

# v1 header: app.cpp compiles. wrapped satisfies Addable.
g++ -std=c++20 -I. app.cpp -L. -lv1 -o app
./app   # β†’ sum<int>(2, 3) = 5

# v2 header: same app.cpp, the addressing of
# `cs_check_addable_only<wrapped>` no longer satisfies the tightened
# concept and the source fails:
g++ -std=c++20 -I. app.cpp -L. -lv2 -o app
# β†’ error: template constraint failure for β€˜template<Addable T>’
# β†’ note: the expression β€˜T()’ would be ill-formed

How to fix (as a library maintainer)

  • Stage the tightening across a deprecation window: ship the looser concept alongside a deprecated alias that warns, then remove.
  • Provide a SFINAE-friendly migration: expose a second template that preserves the old contract, marked [[deprecated]].
  • For internal-only concepts, prefix with detail:: and document them as not-API.

References


Source files

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