Case 111: enumerable_thread_specific Lambda-Init Ambiguity¶
| Field | Value |
|---|---|
| Verdict | ๐ API_BREAK |
| Category | API Break |
| Platforms | Linux, macOS |
| Flags | API break, Bad practice |
Detected ChangeKinds |
func_added |
| Source files | examples/case111_enumerable_thread_specific_lambda_ambiguity/ |
| Detectability | None โ no evidence tier currently proves this verdict (known detector gap) |
| Known detector gap | constructor_overload_ambiguity |
Category: Subtle source break / regression suite | Verdict: ๐ API_BREAK (known detector gap โ abicheck currently reports COMPATIBLE at every evidence tier; see below)
What breaks¶
A second constructor overload is added โ enumerable_thread_specific(int_factory_t)
(a function-pointer-typed factory). By itself this is a pure addition:
existing call sites that pass an int still resolve to the original
constructor. But consumer code patterns that previously had a single
viable conversion path can now become ambiguous, particularly with
brace-initialization or generic callable arguments. The risk is silent โ
code that compiled before may compile to a different constructor against
the new headers, or stop compiling at unrelated call sites that infer
the wrong overload.
Why function-pointer instead of std::function? A realistic
example (such as oneTBB) would accept std::function<int()>, but pulling in
<functional> from libstdc++ 13 trips castxml/clang (__assume__
attribute in <bits/stl_bvector.h>), which would prevent the integration
test from ever running. A function-pointer typedef exhibits the same
overload-ambiguity risk for the purposes of this regression fixture.
Why this matters¶
Mirrors a documented oneTBB pain point: adding lambda-/functor-accepting constructor overloads to existing handle types introduced overload ambiguity in real downstream code. The pattern is repeatable across container-like types.
How abicheck catches it (and where it doesn't)¶
It doesn't โ at any evidence tier. This is the catalog's canonical
example of a scenario being proven true (by the source_smoke oracle
below) while no current detector, at any of L0-L5, produces the verdict
that scenario demands. That is different from case105 (concept tightening)
or case122 (uninstantiated template change), where a higher evidence
tier (L4) does catch the break โ case111 has no tier that catches it yet.
The diff exposes:
FUNC_ADDED: the newstd::function<int()>constructor
FUNC_ADDED on a constructor is, in isolation, compatible โ it cannot
link- or ABI-break anything by itself. The follow-on overload
ambiguity that breaks downstream source compilation depends on the
consumer's call-site context, which no snapshot-level detector currently
reasons about for newly-added constructor overloads (contrast
case169_overload_added's OVERLOAD_ADDED, which only groups
same-named free-function overloads by Itanium mangling โ it does not
reason about constructor-overload call-site ambiguity).
Canonical verdict: API_BREAK โ proven by this case's own
source_smoke (v1 compiles, v2 is ambiguous), matching the project's
definition of API_BREAK: a public-header change that breaks
recompilation while already-built binaries remain viable (abi_break:
false, api_break: true). abicheck's actual output at every evidence
tier is COMPATIBLE with only func_added observed โ a real,
tracked known detector gap, not an evidence-depth limitation. The
gap is recorded so a KINDS_MISMATCH/verdict-mismatch reviewer can see
why the mismatch is expected rather than silently accepting the tool's
current output as ground truth.
A constructor-overload-ambiguity detector is the natural home for closing this gap; it would need the same castxml header-AST capture path used for case105's concept-tightening detector to reason about call-site resolvability.
Update: abicheck has a ChangeKind.CTOR_OVERLOAD_AMBIGUITY_RISK
best-effort heuristic (diff_symbols._diff_ctor_overload_ambiguity)
that flags a class gaining a 2nd+ non-explicit converting constructor โ
the classic implicit-conversion ambiguity pattern. It does not close
this case's gap. Both of this case's constructors are declared
explicit (see v1.h/v2.h), and this case's own source_smoke
proof is triggered by empty-brace-list direct-initialization
(ets({})), not implicit conversion โ direct-initialization performs
overload resolution over explicit constructors too, and an empty
braced-init-list value-initializes almost any scalar/pointer parameter
type, so it collides across both new and old overloads regardless of
explicit. Soundly detecting that would need a general call-site
overload-resolution simulation (which argument shapes are viable
against which parameter types), not a snapshot-level heuristic โ still
future work. The heuristic was deliberately scoped to the narrower,
lower-false-positive non-explicit case rather than widened to cover
this scenario, since widening to any 2nd single-scalar-argument
constructor (explicit or not) would fire on most multi-constructor
classes.
Code diff¶
| v1 | v2 |
|---|---|
enumerable_thread_specific(int); |
same โ plus a new overload |
| (no other ctors) | enumerable_thread_specific(int_factory_t); (typedef int (*int_factory_t)()) |
How to fix (as a library maintainer)¶
- Constrain the lambda-init overload with a SFINAE / concept that
excludes
int-convertible types. e.g.: This eliminates the ambiguity at the call site. - Or expose the lambda-init as a named factory
(
from_lambda(...)) rather than as an overloaded constructor.
References¶
- oneTBB issue tracker โ overload ambiguity in
enumerable_thread_specificconstructor set.
Source files¶
CMakeLists.txtapp.cppv1.cppv1.hv2.cppv2.h
See also: Examples overview ยท All API_BREAK cases ยท Category: API Break.