llvm / llvm/llvm-project

`requires` expression result changes depending on prior template instantiation order

Open
#186,213 5 comments 0 reactions 0 assignees View on GitHub
clang:frontend concepts needs-reduction
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

# `requires` expression result changes depending on prior template instantiation order

## Description

The result of a `requires` expression checking aggregate initialization validity changes depending on which types have been previously checked. Evaluating `can_init` followed by `can_init` causes the second to return `false`, even though both should return `true`. Swapping the evaluation order causes the first to pass and the second to fail. Either type checked in isolation passes.

## Minimal Reproducer

```cpp
#include
#include
#include
#include

struct any_t {
template
constexpr operator T() const;
};

template
constexpr bool can_init = requires { T{any_t{}}; };

struct B;
using V = std::variant;
struct W { std::unique_ptr p; };

struct A { int x; std::tuple y; };
struct B { int x; std::tuple y; };

static_assert(can_init); // OK
static_assert(can_init); // FAILS: evaluates to false
```

Both `A` and `B` are 2-member aggregates. `T{any_t{}}` should be a valid aggregate initialization for both: the first member is initialized from `any_t{}` and the second member (a `std::tuple`) is value-initialized. Each `static_assert` passes when the other is removed.

Swapping the two `static_assert` lines causes `can_init` to fail instead:

```cpp
static_assert(can_init); // OK (now evaluated first)
static_assert(can_init
); // FAILS (now evaluated second)
```

## Expected Behavior

Both `static_assert`s pass regardless of order.

## Actual Behavior

Whichever `can_init` is evaluated second always fails.

## Compiler Versions

- **Fails:** Apple Clang 17.0.0 (clang-1700.4.4.1), arm64-apple-darwin25.3.0 (also tested on compiler explore with Clang trunk for x86)
- **Passes:** GCC 14.3.0

## Notes

During evaluation of the first `can_init`, Clang instantiates `any_t::operator W()` as part of overload resolution inside `std::tuple`'s constructor, producing the warning:

```
warning: inline function 'any_t::operator W' is not defined [-Wundefined-inline]
```

This appears to create cached template instantiation state that corrupts the `requires` evaluation for the second type.

All of the following conditions are necessary to trigger the bug:

- `std::unique_ptr` (raw pointer and `std::shared_ptr` do not trigger it)
- `std::variant` containing a forward-declared type that is self-referential (the struct is in its own variant)
- `std::tuple` wrapping (not `std::pair` or direct struct members)
- Two structs with **different** `std::tuple` arities of the same wrapper type
- Both `can_init` evaluations present (either alone passes)

This may be related to [#134148](https://github.com/llvm/llvm-project/issues/134148), which reports an assertion failure in `SemaTemplateInstantiate.cpp` triggered by a similar pattern.

This bug was discovered while investigating [Glaze #2234](https://github.com/stephenberry/glaze/issues/2234), where it causes the library's compile-time member counting to silently return 0 for affected types.

Contributor guide

Open the contributing guide

Research direction

Start by compiling the minimal reproducer with the affected Clang and reversing the two static_assert evaluations to confirm the order-dependent requires result. Trace can_init through aggregate initialization, std::tuple construction, and any_t::operator W; done means both assertions pass in either order without the cached-instantiation failure.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.