[clang][22.x regression] Constrained friend function template inside a class template: enclosing-class type alias binds to the wrong instantiation during ADL across instantiations
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Summary
Starting with Clang 22, a constrained `friend` function template defined inside a class template fails when two *different* instantiations of the enclosing class template participate in the same ADL call. The constraint that anchors the friend to its enclosing class via `std::same_as` is evaluated as if `EnclosingType` referred to *the same* instantiation in every ADL candidate, so the candidate from the "correct" instantiation is rejected. Clang 21 and earlier (as well as GCC 14/15) accept the code.
This is reproducible on **clang trunk** (LLVM main) and is **not** an ABI / libc++ issue — the reproducer below uses only the language frontend with locally-defined concepts.
## Minimal reproducer
[godbolt](https://godbolt.org/z/f14hbh93z) — clang trunk / 22.1.0 / 21.1.0 / gcc 15.1.0 side-by-side.
```cpp
#include
#include
struct base_marker {};
template
struct outer {
struct inner : base_marker {
template
requires std::derived_from, base_marker> &&
std::same_as, inner>
friend void operator|(L&&, S&&) {}
};
};
int main() {
typename outer::inner a;
typename outer::inner b;
a | b;
}
```
### Expected behaviour
For the call `a | b`:
* ADL finds `friend operator|` in both `outer::inner` and `outer::inner`.
* In the candidate contributed by `outer::inner`, `inner` (the enclosing class) denotes `outer::inner`. Constraint `same_as` becomes `same_as::inner, outer::inner>` → **false** → candidate rejected.
* In the candidate contributed by `outer::inner`, `inner` denotes `outer::inner`. Constraint becomes `same_as::inner, outer::inner>` → **true** → candidate viable.
* Exactly one viable candidate; the call compiles.
This matches what Clang 21 and GCC 14/15 do.
### Observed behaviour (Clang 22.x and trunk)
```
:33:5: error: invalid operands to binary expression
('typename outer::inner' and 'typename outer::inner')
33 | a | b;
| ~ ^ ~
:26:17: note: candidate template ignored: constraints not satisfied
[with L = typename outer::inner &,
S = typename outer::inner &]
26 | friend void operator|(L &&, S &&) {}
| ^
:25:16: note: because
'std::same_as::inner &>, inner>'
evaluated to false
25 | std::same_as, inner>
| ^
…/concepts:59:27: note: because
'std::is_same_v::inner, outer::inner>'
evaluated to false
:26:17: note: candidate template ignored: constraints not satisfied
[with L = typename outer::inner &,
S = typename outer::inner &]
… (same diagnostic repeated for the second candidate)
```
Note that the second `note: because 'std::is_same_v::inner, outer::inner>'` is identical to the first — Clang is comparing against `outer::inner` in **both** candidates, so the friend originating from `outer::inner` is also rejected. The reference to `inner` inside the friend's constraint has been bound to `outer::inner` regardless of which instantiation contributed the candidate.
## Compiler matrix
| Compiler | Result |
| --------------- | -------------- |
| clang trunk | ❌ reject |
| clang 22.1.0 | ❌ reject |
| clang 21.1.0 | ✅ accept |
| gcc 15.2.0 | ✅ accept |
| gcc 14.3.0 | ✅ accept |
Tested locally with MacPorts `clang++-mp-21`, `clang++-mp-22`, `g++-mp-14`, `g++-mp-15` and on godbolt with `clang_trunk`, `clang2210`, `clang2110`, `g151`. Standard option (`-std=c++20`/`c++23`/`c++26`) does not change the result. Replacing `std::same_as` / `std::derived_from` / `std::remove_cvref_t` with hand-written equivalents in a fresh namespace still reproduces — this is independent of libc++.
`clang_assertions_trunk` produces the same diagnostic with no internal assertion firing, so this appears to be a deliberate code path rather than an obvious null/invariant violation.
## Origin (for searchability)
First observed when building [`facebookexperimental/libunifex`](https://github.com/facebookexperimental/libunifex) (commit `75180df5`) — every call site that pipes a `bind_back` result into another `bind_back` result via `operator|` fails to compile on Clang 22. `libunifex/bind_back.hpp` uses exactly this in-class constrained friend pattern with `same_as, type>` (where `type` is the enclosing class itself). In the libunifex case the failure manifests as `error: use of overloaded operator '|' is ambiguous` rather than `no viable candidate` — both diagnostics share the same root cause but differ because libunifex's constraints add a `member_t` chain that takes constraint substitution down a different code path.
Contributor guide
Assessment
This issue has not been assessed yet.