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
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¶
- P0892R2
conceptdefinition syntax - cppreference:
requires-expression - castxml limitation: concepts emitted as
<Unimplemented kind="Concept"/>(no name, no body) β see castxml issue tracker.
Source files¶
See also: Examples overview Β· All COMPATIBLE cases Β· Category: Addition (Compatible).