llvm / llvm/llvm-project

[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

Open
#199,325 5 comments 0 reactions 1 assignee Claimed by @zyn0217 View on GitHub
clang:frontend concepts needs-reduction regression:22
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.