https://gcc.gnu.org/bugzilla/show_bug.cgi?id=125601
Bug ID: 125601
Summary: `break` in a `template for` escapes to the enclosing
loop during constant evaluation
Product: gcc
Version: 16.1.1
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: c++
Assignee: unassigned at gcc dot gnu.org
Reporter: garcia.6l20 at gmail dot com
Target Milestone: ---
I ran into this writing an enum-flags parser. The code worked in debug builds
and broke in release, and it took me a while to realize the trigger is constant
evaluation, not optimization itself.
## What's going on
If a `break` sits inside a `template for` that is itself inside a `while` loop,
the `break` should end the `template for` and let the `while` keep going. That
is what happens at runtime. But when the function is evaluated as a constant
expression, the `break` leaves the `while` as well, so the loop runs only once.
[[stmt.break](https://eel.is/c++draft/stmt.break)] is clear that the expansion
statement is what gets terminated:
> A `break` statement shall be enclosed by ([stmt.pre]) an *iteration-statement*
> ([stmt.iter]) or a `switch` statement ([stmt.switch]) or an
> *expansion-statement* ([stmt.expand]). The `break` statement causes
> termination of the innermost such enclosing statement; control passes to the
> statement following the terminated statement, if any.
The innermost enclosing statement of the `break` here is the `template for`, so
it must not touch the `while`.
## Reproducer
Self-contained, no includes and no reflection needed:
```cpp
constexpr int count_while_iterations()
{
int iterations = 0;
int remaining = 3;
while (remaining > 0)
{
++iterations;
--remaining;
template for (constexpr int value : {10, 20, 30})
{
(void)value;
break; // should end the template for only, not the while
}
}
return iterations;
}
static_assert(count_while_iterations() == 3); // should pass, but fails
int main()
{
return count_while_iterations() == 3 ? 0 : 1;
}
```
Build with:
```sh
g++ -std=gnu++26 reproducer.cpp
```
The `static_assert` fails:
```
reproducer.cpp:39:40: error: static assertion failed
39 | static_assert(count_while_iterations() == 3);
| ~~~~~~~~~~~~~~~~~~~~~~~~~^~~~
* the comparison reduces to '(1 == 3)'
```
`count_while_iterations()` should be 3: the `while` runs three times, adding
one
each pass, and the inner `break` only leaves the `template for`. The constant
evaluator returns 1, because it leaves the `while` on the first `break`.
## Why debug built fine and release didn't
The generated code is correct. The bug is only in the constant evaluator. What
decides whether you hit it is whether the call gets constant-folded.
Comment out the `static_assert` and run the program:
g++ -std=gnu++26 -O0 reproducer.cpp && ./a.out ; echo $? -> 0 (call
returned 3)
g++ -std=gnu++26 -O1 reproducer.cpp && ./a.out ; echo $? -> 1 (call
returned 1)
At `-O0` the call is emitted as real code and returns the correct 3. At `-O1`
and above GCC folds this fully-constant call through the constant evaluator and
bakes in the wrong answer. You can see it in the `-O1` assembly, where `main`
is
just the folded constant:
```asm
main:
movl $1, %eax
ret
```
That is the whole debug-vs-release story: release optimization folds the call,
which is what drags it into the broken evaluator.
To double-check that codegen itself is fine, take the loop count from `argc` so
the call can never be folded:
```cpp
constexpr int count_while_iterations(int remaining)
{
int iterations = 0;
while (remaining > 0)
{
++iterations;
--remaining;
template for (constexpr int value : {10, 20, 30})
{
(void)value;
break;
}
}
return iterations;
}
int main(int argc, char**)
{
return count_while_iterations(argc + 2) == 3 ? 0 : 1;
}
```
This returns 0 at every optimization level, and the `-O1` asm actually runs the
loop instead of folding it:
```asm
main:
leal 2(%rdi), %eax
testl %eax, %eax
jle .L4
.L3:
subl $1, %eax
jne .L3
addl $2, %edi
.L2:
cmpl $3, %edi
setne %al
movzbl %al, %eax
ret
```
So the runtime path is correct, only the constant evaluator is wrong.
## Versions
Same behavior on both compilers I tried:
- `g++ (GCC) 16.1.1 20260430`
- `g++ (GCC) 17.0.0 20260602` (trunk on compiler explorer)
- `-std=gnu++26` (or `-std=c++26`), x86_64 Linux
## Where I ran into it
The original code is an enum-flags parser. It walks `|`-separated tokens in an
outer `while`, and for each token scans the enumerators with a `template for`,
`break`-ing as soon as it matches. Parsing `"Read|Execute"` only ever returned
`Read` in release builds, where the call got constant-folded, but was fine in
debug builds, where it ran at runtime. Probably worth filing under component
`c++` and linking to PR 120776 (P1306R5 expansion statements).