1. Revision history
1.1. Since R0
The paper was seen by EWGI and then by EWG at Hagenberg 2025, with the following polls:
P3568R0: EWG likes syntax
N3355 : for (...) { }
SF F N A SA 4 16 5 9 5 P3568R0: EWG likes syntax
for N3377 (...) { }
SF F N A SA 7 13 5 5 8 P3568R0: If C has it, we are interested in this feature too.
SF F N A SA 16 21 5 2 1
Due to lack of consensus in EWG, syntactical design choices were delegated to WG14. WG14 saw [N3377] at Graz 2025 and voted as follows:
N3377: Would WG14 like to see a paper changing loop name syntax at a future meeting?
F N A 6 11 9
The authors of [N3377] have expressed that they are no longer pursuing the paper; therefore, R1 of this paper assumes that the debate on label syntax is entirely settled, and C++ follows the [N3355] syntax.
Furthermore, the proposed wording has been improved slightly,
and a
feature-test macro is now included.
2. Introduction
While C++ already has a broad selection of control flow constructs,
one construct commonly found in other languages is notably absent:
the ability to apply
or
to a loop or
when
this isn’t the innermost enclosing statement.
This feature is popular, simple, and quite useful:
Specifically, we propose the following functionality:
outer : for ( auto x : xs ) { for ( auto y : ys ) { if ( /* ... */ ) { continue outer ; // OK, continue applies to outer for loop break outer ; // OK, break applies to outer for loop } } } switch_label : switch ( /* ... */ ) { default : while ( true) { if ( /* ...*/ ) { break switch_label ; // OK, break applies to switch, not to while loop } } } break outer ; // error: cannot break loop from the outside goto outer ; // OK, used to be OK, and is unaffected by this proposal switch_label :; // OK, labels can be reused goto switch_label ; // error: jump target is ambiguous
The
and
syntax is identical to that in [N3355] and has been accepted into C2y (see working draft at [N3435]).
We bring that syntax into C++ and relax restrictions on labels to
make it more powerful,
and to address concerns in a follow-up proposal [N3377].
Note that
and
with labels have been proposed in [N3879] and rejected at Rapperswil 2014 ([N4327]):
Straw poll, proposal as a whole:
SF F N A SA 1 1 1 13 10 "break label;" + "continue label;"
SF F N A SA 3 8 4 9 3
I believe that rejecting
was a grave mistake at a time.
Regardless, the WG21 sentiment towards the feature is now the opposite,
even if just for C compatibility (see § 1.1 Since R0).
3. Motivation
and
are largely motivated by the ability to control nested loops.
This is a highly popular feature in other languages,
and C++ could use it too, since it has no good alternative.
To be fair, a conditional
in the loop sometimes bypasses the need to terminate it.
However, this is not always allowed; such practice is outlawed by
MISRA-C++:2008 Rule 6-6-5 "A function shall have a single point of exit at the end of the function"
([MISRA-C++]).
Even if it is permitted, there are many cases where an early
does not obsolete
, and it generally does not obsolete
.
Note: I have been told that more recent revisions of MISRA-C++ no longer include this rule.
3.1. No good alternative
Let’s examine a motivating example which uses our new construct:
void f () { process_files : for ( const File & text_file : files ) { for ( std :: string_view line : text_file . lines ()) { if ( makes_me_angry ( line )) { continue process_files ; } consume ( line ); } std :: println ( "Processed {}" , text_file . path ()); } std :: println ( "Processed all files" ); }
is very useful in this scenario,
and expresses our intent with unparalleled clarity.
We want to continue processing other files, so we
.
A plain
cannot be used here because it would result in executing the
following
statement, but this should only be done upon success.
There are alternative ways to write this, but all of them have various issues.
3.1.1. goto
for ( const File & text_file : files ) { for ( std :: string_view line : text_file . lines ()) { if ( makes_me_angry ( line )) { goto done_with_file ; } consume ( line ); } std :: println ( "Processed {}" , text_file . path ()); done_with_file : } std :: println ( "Processed all files" );
is similar in complexity and even readability here, however there are some issues:
-
cannot cross (non-vacuous) initialization, which would be an issue if some variable was initialized prior togoto
. This can be addressed by surrounding the outer loop contents with another set of braces, but this solution isn’t obvious and takes away from the elegance ofstd :: println
here.goto -
cannot be used in constant expressions. For processing text files like in the example, this doesn’t matter, but nested loops are desirable in agoto
context as well.constexpr -
Many style guides ban or discourage the use of
. See [MISRA-C++], [CppCoreGuidelinesES76], etc. This discouragement dates all the way back to 1968 (see [GotoConsideredHarmful]), and 66 years of teaching not to usegoto
won’t be undone.goto -
Even in the cases where
isn’t discouraged, those cases are always special, like "onlygoto
forwards", "onlygoto
to break out of loops", etc.. This issue has been debated for decades, and there is still no consensus on when, actually,goto
is okay to use.goto -
is innately more difficult to use because to understand its purpose, the user has to know where the jump target is located. Agoto
behaves radically differently compared to agoto past_the_loop
. Moving the jump target or thegoto before_the_loop
statement relative to each other can also completely change these semantics. By comparison,goto
andbreak
always jump forwards, past a surrounding loop, or to the end of a surrounding loop respectively. This makes them much easier to reason about, and much less error-prone.continue -
The "local readability" of
relies heavily on high-quality naming for the label. Agoto
could mean to the end of a loop, to after the loop, to the end of a function, etc. Sincegoto end
andbreak
are much more limited, they do not require such good label naming. Acontinue
has bad name, but the user generally understands its purpose.break loop
Note: Previous discussion on the [isocpp-core] reflector has addressed the idea
of just adding
,
but doing so is alleged to be more complicated than more limited
control flow
structures which can only "jump forwards", such as
and
.
In conclusion, there are too many issues with
, some of which may never be resolved. [std-proposals] discussion prior to the publication of this proposal has shown once again
that
is a controversial and divisive.
3.1.2. Immediately invoked lambda expression (IILE)
for ( const File & text_file : files ) { [ & ] { for ( std :: string_view line : text_file . lines ()) { if ( makes_me_angry ( line )) { return ; } consume ( line ); } std :: println ( "Processed {}" , text_file . path ()); }(); } std :: println ( "Processed all files" );
While this solution works in constant expressions,
we may be painting ourselves into a corner with this design.
We cannot also
the surrounding loop from within the IILE,
and we cannot return from the surrounding function.
If this is needed at some point, we will have to put substantial effort into refactoring.
Furthermore, this solution isn’t exactly elegant:
-
The level of indentation has unnecessarily increased through the extra scope.
-
The call stack will be one level deeper during debugging. This may be relevant to debug build performance.
-
The fact that the lambda is immediately invoked isn’t obvious until reading up to
.() -
The word
does not express the overall intent well, which is merely to continue the outer loop. This can be considered a teachability downside.return
It is also possible to use an additional function instead of an IILE in this place. However, this is arguably increasing the degree of complexity even more, and it scatters the code across multiple functions without any substantial benefit.
3.1.3. Mutable bool
state
for ( const File & text_file : files ) { bool success = true; for ( std :: string_view line : text_file . lines ()) { if ( makes_me_angry ( line )) { success = false; break ; } consume ( line ); } if ( success ) { std :: println ( "Processed {}" , text_file . path ()); } } std :: println ( "Processed all files" );
This solution substantially increases complexity. Instead of introducing extra scope and call stack depth, we add more mutable state to our function. The original intent of "go process the next file" is also lost.
Such a solution also needs additional state for every nested loop,
i.e. N
s are needed to
from N nested loops.
3.2. Constant expressions
Use of
has become tremendously more common,
and
may not be used in constant expressions.
Where
is used to break out of nested loops,
makes it easy to migrate code:
goto
to break out of nested loops can be replaced with break label
as follows:
constexpr void f () {
outer : while ( /* ... */ ) {
while ( /* ... */ ) {
if ( /* ... */ ) {
goto after_loop ;
break outer ;
}
}
}
after_loop :;
}
Due to reasons mentioned above,
I do not believe that "
" is a path forward that will find consensus.
3.3. Argumentum ad populum
Another reason to have
and
is simply that it’s a
popular construct, available in other languages.
When Java, JavaScript, Rust, or Kotlin developers pick up C++,
they may expect that C++ can
out of nested loops as well,
but will find themselves disappointed.
[StackOverflow] "Can I use break to exit multiple nested
loops?" shows that there is interest in this feature (393K views at the time of writing).
A draft of the proposal was posted on [Reddit] and received overwhelmingly positive feedback (70K views, 143 upvotes with, 94% upvote rate at the time of writing).
3.3.1. Poll
Another way to measure interest is to simply ask C++ users. The following is a committee-style poll (source: [TCCPP]) from the Discord server Together C & C++, which is the largest server in terms of C++-focused message activity:
Should C++ have "break label" and "continue label" statements to apply break/continue to nested loops or switches?
SF F N A SA 21 21 12 6 4
Note: 64 users in total voted, and the poll was active for one week.
3.3.2. How common is break
/continue
with labels?
To further quantify the popularity, we can use GitHub code search for various
languages which already support this feature.
The following table counts only control statements with a label, not plain
,
, etc.
We also count statements like Perl’s
;
it is de-facto
, just with a different spelling.
Language | Syntax | Labeled s
| Labeled s
| Σ
|
Java |
| 424K files | 152K files | 576K files |
JavaScript |
| 53.8K files | 68.7K files | 122.5K files |
Perl |
| 34.9K files | 31.7K files | 66.6K files |
Rust |
| 30.6K files | 29.1K files | 59.7K files |
TypeScript |
| 11.6K files | 9K files | 20.6K files |
Swift |
| 12.6K files | 5.6K files | 18.2K files |
Kotlin |
| 8.7K files | 7.6K files | 16.3K files |
D |
| 3.5K files | 2.6K files | 6.1K files |
Go |
| 270 files | 252 files | 522 |
Ada |
| N/A | N/A | N/A |
Dart |
| N/A | N/A | N/A |
Cpp2 (cppfront) |
| N/A | N/A | N/A |
C |
| N/A | N/A | N/A |
Fortran |
| N/A | N/A | N/A |
Groovy |
| N/A | N/A | N/A |
Odin |
| N/A | N/A | N/A |
PL/I |
| N/A | N/A | N/A |
PostgreSQL |
| N/A | N/A | N/A |
PowerShell |
| N/A | N/A | N/A |
Based on this, we can reasonably estimate that there are at least one million files
in the world which use labeled
/
(or an equivalent construct).
Note: This language list is not exhaustive and the search only includes open-source code bases on GitHub. Some of the cells are N/A because the number isn’t meaningful, or simply because I haven’t gotten around to doing the code search yet.
3.4. C2y compatibility
Last but not least, C++ should have
and
to increase the amount of code that has a direct equivalent in C.
Such compatibility is desirable for two reasons:
-
functions or macros used in C/C++ interoperable headers could use the same syntax.inline -
C2y code is much easier to port to C++ (and vice-versa) if both languages support the same control flow constructs.
Furthermore, the adoption of [N3355] saves EWG a substantial amount of time when it comes to debating the syntax; the C++ syntax should certainly be C-compatible.
4. Design Considerations
4.1. Alternative break
and continue
forms
While the idea of applying
and
to some surrounding construct of choice is simple,
there are infinite ways to express this.
Various ideas have been proposed over the last months and years:
-
,break 1
, ... (specify the amount of loops, not the targeted loop)break 2 -
,break while
, ... (target by keyword, not by label)break for while -
(execute statement in the jumped-to scope)break break -
(competing syntax in [N3377])for name (...)
All of these have been discussed in great detail in the first revision of this paper, [P3568R0]. At this point, it would be a waste of time to discuss these in detail.
WG21 overwhelmingly agrees (based on polls, reflector discussions, and personal conversations) that the design should be compatible with C. This is also reflected by a poll at Hagenberg 2025:
P3568R0: If C has it, we are interested in this feature too.
SF F N A SA 16 21 5 2 1
Furthermore, WG14 has already accepted the
syntax of [N3355] into C2y, and WG14 is unwilling to revisit this syntax,
as voted at Graz 2025:
N3377: Would WG14 like to see a paper changing loop name syntax at a future meeting?
F N A 6 11 9
There is only one way forward that has a chance of finding consensus: do what C does.
4.2. Changes to labels
While the proposed
syntax of [N3377] was de-facto rejected at Graz,
the paper brings up legitimate issues with C2y
after [N3355].
Notably, the restriction that a label can be used only once per function is not usually
present in other languages that support
.
This restriction is especially bad for C and C++ because if
was used in a macro,
that macro could only be expanded once per function:
#define MACRO() outer: for ( /* ... */ ) for ( /* ... */ ) break outer; void f () { MACRO () // OK so far MACRO () // error: duplicate label 'outer' }
The author of [N3355] has expressed to me that he intends to address these label issues for C2y. In parallel, this proposal addresses such issues by relaxing label restrictions. Presumably, C and C++ will converge on identical restrictions.
4.2.1. Design philosophy
The proposed design is extremely simple:
Drop all restrictions on labels.
Make
and
break label "just work" anyway.
continue label Disallow
for duplicate
goto label .
label
Any existing
code remains unaffected by this change.
These rules are simple and easy to remember.
While it may seem too lax to put no restrictions on labels at all,
there’s no obvious problem with this.
Labels don’t declare anything, and unless referenced by
and
,
they are de-facto comments with zero influence on the labeled code.
If labels are quasi-comments, why should there be any restrictions on the labels themselves?
The consequences and details of these changes are described below.
4.2.2. Allowing duplicate labels
I propose to permit duplicate labels, which makes the following code valid:
outer : while ( true) { inner : while ( true) { break outer ; // breaks enclosing outer while loop } } outer : while ( true) { // OK, reusing label is permitted inner : while ( true) { break outer ; // breaks enclosing outer while loop } } goto outer ; // error: ambiguous jump target
Such use of duplicate labels (possibly with different syntax)
is permitted in numerous other languages,
such as Rust, Kotlin, Java, JavaScript, TypeScript, Dart, and more.
To be fair, languages that also support
require unique labels per function,
but there’s no technical reason why the uniqueness restriction
couldn’t be placed on
rather than the labels themselves.
As mentioned before, permitting such code is especially useful when these loops are not
hand-written, but expanded from a macro.
Even disregarding macros, there’s nothing innately wrong about this code,
and it is convenient to reuse common names like
for controlling nested loops.
Note: Existing code using
is unaffected
because existing code cannot have duplicate labels in the first place.
4.2.3. Reusing labels in nested loops
A more controversial case is the following:
l : while ( true) { l : while ( true) { break l ; // equivalent to break; } }
generally applies to the innermost loop labeled
,
so the inner loop is targeted here.
I believe that this code should be valid
because it keeps the label restrictions stupidly simple (there are none),
and because this feature may be useful to developers.
One may run into this case when nesting pairs of
/
loops in each other "manually",
or when an
-labeled loop in a macro is expanded into a surrounding loop that also uses
.
Note: This code is not valid Java or JavaScript, but is valid Rust when using the label '
.
4.2.4. Duplicate labels on the same statement
A more extreme form of the scenario above is:
l : l : l : l : f ();
I also believe that this code should be valid because it’s not harmful, and may be useful in certain, rare situations (see below). Once again, allowing it keeps the label restrictions stupidly simple.
// common idiom on C: expand loops from macros #define MY_LOOP_MACRO(...) outer: for ( /* ... */ ) outer : MY_LOOP_MACRO ( /* ... */ ) { break outer ; }
If
already uses an
label internally,
perhaps because it expands to two nested loops and uses
itself,
then the macro effectively expands to
.
This forces the user to come up with a new label now,
for no apparent reason.
4.2.5. break label
for loops with more than one label
Another case to consider is this:
x : y : while ( true) { break x ; // OK in C2y }
[N3355] makes wording changes to C so that the code above is valid.
For C2y compatibility and convenience, we also make this valid.
We don’t change the C++ grammar to accomplish this,
but define the term (to) label (a statement),
where
labels
.
5. Impact on existing code
No existing code becomes ill-formed or has its meaning altered. This proposal merely permits code which was previously ill-formed, and relaxes restrictions on the placement of labels.
6. Implementation experience
An LLVM implementation is W.I.P.
A GCC implementation of [N3355] has also been committed at [GCC].
7. Proposed wording
The wording is relative to [N5001].
Update [stmt.label] paragraph 1 as follows:
A label can be added to a statement or used anywhere in a compound-statement.label:The optional attribute-specifier-seq appertains to the label.
attribute-specifier-seqopt identifierlabeled-statement:
:
attribute-specifier-seqoptconstant-expression
case
:
attribute-specifier-seqopt
default
:
label statementThe only use of a label with an identifier is as the target of aA label can be used in a. No two labels in a function shall have the same identifier.
goto statement ([stmt.goto]) before its introduction.
goto
[ Note: Multiple identical labels within the same function are permitted, but such duplicate labels cannot be used in astatement. — end note ]
goto
In [stmt.label] insert a new paragraph after paragraph 1:
A label L of the form attribute-specifier-seqopt identifierlabels the statement S of a labeled-statement X if
:
- L is the label of X, or
- L labels X (recursively).
[ Example:— end example ]a : b : while ( 0 ) { } // both a: and b: label the loop c : { d : switch ( 0 ) { // unlike c:, d: labels the switch statement default : while ( 0 ) { } // default: labels nothing } }
Note: This defines the term (to) label, which is used extensively below.
We also don’t want
or
labels to label statements, since this would inadvertently
permit
given
, considering how we word [stmt.break].
Update [stmt.label] paragraph 3 as follows:
A control-flow-limited statement is a statement S for which:
- a
or
case label appearing within S shall be associated with a
default statement ([stmt.switch]) within S, and
switch - a label declared in S shall only be referred to by a statement
([stmt.goto])in S.
Note: While the restriction still primarily applies to
(preventing the user from e.g. jumping into an
statement),
if other statements can also refer to labels, it is misleading to say
"statement ([stmt.goto])" as if
was the only relevant statement.
Update [stmt.jump.general] paragraph 1 as follows:
Jump statements unconditionally transfer control.jump-statement:identifier
goto
;
identifieropt
break
;
identifieropt
continue
;
expr-or-braced-init-listopt
return
;
identifier
goto
;
Note:
is being relocated to the top so that all the jump statements with an identifier are grouped together.
Of these three,
is being listed first because it models the concept of
"jumping somewhere" most literally; every following statement is more sophisticated or even
defined as equivalent to
(in the case of
).
Update [stmt.break] paragraph 1 as follows:
A breakable statement is an iteration-statement ([stmt.iter]) or astatement ([stmt.switch]). A
switch statement shall be enclosed by ([stmt.pre]) a breakable statement
break an iteration-statement ([stmt.iter]) or a. If present, the identifier shall be part of a label L which labels ([stmt.label]) an enclosing breakable statement. Thestatement ([stmt.switch])
switch statement causes termination of :
break the smallest such enclosing statement;
- if an identifier is present, the smallest enclosing breakable statement labeled by L,
- otherwise, the smallest enclosing breakable statement.
controlControl passes to the statement following the terminated statement, if any.
[ Example:— end example ]a : b : while ( /* ... */ ) { a : a : c : for ( /* ... */ ) { break ; // OK, terminates enclosing for loop break a ; // OK, same break b ; // OK, terminates enclosing while loop y : { break y ; } // error: break does not refer to a breakable statement } break c ; // error: break does not refer to an enclosing statement } break ; // error: break is not enclosed by a breakable statement
Update [stmt.cont] paragraph 1 as follows:
Astatement shall be enclosed by ([stmt.pre]) an iteration-statement ([stmt.iter]). If present, the identifier shall be part of a label L which labels ([stmt.label]) an enclosing iteration-statement. The
continue statement causes control to pass to the loop-continuation portion of :
continue the smallest such enclosing statement, that is, to the end of the loop.More precisely, in each of the statements
- if an identifier is present, the smallest enclosing iteration-statement labeled by L,
- otherwise, the smallest enclosing iteration-statement.
label : while ( foo ) { { // ... } contin : ; } label : do { { // ... } contin : ; } while ( foo ); label : for (;;) { { // ... } contin : ; } athe following are equivalent tonot contained in an an enclosed iteration statement is equivalent to
continue .
goto contin :
goto contin
- A
not contained in an an enclosed iteration statement.
continue - A
not contained in an enclosed iteration statement labeled
continue label .
label :
Note: The clarification "that is, to the end of the loop"
was dropped entirely based on community feedback.
"the end of the loop" is not all that much clearer either, and the whole
equivalence portion
should make it clear enough what the behavior is.
Update [stmt.goto] paragraph 1 as follows:
Thestatement unconditionally transfers control to
goto thea statement labeled ([stmt.label]) bythe identifiera label in the current function containing identifier .The identifier shall be a label located in the current function.There shall be exactly one such label.
:
, and an identifier in itself would never match the label rule. Add a feature-test macro to [tab:cpp.predefined.ft] as follows:
Macro name Value
__cpp_break_label
20 ???? L
8. Acknowledgements
I thank Sebastian Wittmeier for providing a list of languages that support both
and
/
with the same label syntax.
I think Arthur O’Dwyer and Jens Maurer for providing wording feedback and improvement suggestions.
I especially thank Arthur O’Dwyer for helping me expand the list in § 3.3.2 How common is break/continue with labels?. An even more complete list may be available at [ArthurBlog].
I thank the Together C & C++ community for responding to my poll; see [TCCPP].