[clang][bytecode] Implicit definition of a defaulted constructor is not available to the bytecode interpreter when the class comes from a module
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
Clang 22.1.8 (Debian, `22.1.8~++20260714014842+ca7933e47d3a`), target `x86_64-pc-linux-gnu`.
With `-fexperimental-new-constant-interpreter`, a defaulted constructor of a class imported
from a C++20 module is reported as undefined when it has to run during constant evaluation.
The default evaluator accepts the same code.
### Reproducer, without the standard library
`m.cppm`:
```cpp
export module m;
export struct allocator_like {
constexpr allocator_like() noexcept = default;
};
export struct box {
allocator_like allocation;
int value = 42;
constexpr box() = default;
[[nodiscard]] constexpr int get() const { return value; }
};
```
`use.cpp`:
```cpp
import m;
consteval int evaluate() {
box b;
return b.get();
}
static_assert(evaluate() == 42);
int main() {}
```
```sh
clang++ -std=c++23 --precompile m.cppm -o m.pcm
clang++ -std=c++23 -fmodule-file=m=m.pcm -fsyntax-only use.cpp
clang++ -std=c++23 -fmodule-file=m=m.pcm -fsyntax-only -fexperimental-new-constant-interpreter use.cpp
```
The first compilation is accepted. The second reports:
```
use.cpp:6:15: error: static assertion expression is not an integral constant expression
m.cppm:10:13: note: undefined constructor 'allocator_like' cannot be used in a constant expression
```
### What it means in practice
The same thing stops any translation unit that uses `import std;` and constructs a
`std::vector` during constant evaluation:
```cpp
import std;
consteval std::size_t evaluate() {
std::vector order;
order.push_back(1);
order.push_back(2);
return order.size();
}
static_assert(evaluate() == 2);
int main() {}
```
```
__vector/vector.h:134:55: note: undefined constructor 'allocator' cannot be used in a constant expression
__memory/allocator.h:80: note: declared here
```
It is that one specialisation. With libc++'s `std` module, element types `char`, `int`,
`long`, `unsigned`, `unsigned long long` and `std::uint32_t` all evaluate; `std::size_t`
(`unsigned long`, which libc++ instantiates inside its own module) does not.
### What narrows it down
- A class from a module whose defaulted constructor has to call the defaulted constructor of
a member of class type: **fails**.
- The same class with the constructor written out (`constexpr allocator_like() noexcept {}`):
accepted.
- A class from a module with a defaulted constructor and no member of class type: accepted.
- Making the module itself use the constructor -- `namespace { constexpr box produced_here{}; }`
inside `m.cppm` -- makes the failing case pass. That suggests the implicit definition is
serialised into the BMI only when the module odr-uses it, and that the importer cannot
produce one for this interpreter. The default evaluator produces it either way.
- Adding a use in the importer instead -- a namespace-scope variable, an ordinary function, or
a `constexpr` variable of the type -- does not help.
Contributor guide
Research direction
Reproduce the failure with m.cppm and use.cpp using the three clang++ commands, then compare the default evaluator with -fexperimental-new-constant-interpreter. Trace how the defaulted constructor and its class-type member are serialized and made available across the module BMI. Done means the importer accepts the shown static_assert and a regression test covers the module case.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100