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).

Reply via email to