---
title: Case ranges
document: P4040R0
date: 2026-04-01
audience: SG22
reply-to:
  - "Jan Schultke < <janschultke@gmail.com>"
paper-type: proposal
---

C2y added ranges in `case` labels, such as `case 0 ... 9:`. Such case ranges have also been supported as a C++ compiler extension for many years. This feature should be standardized for C++.



## Motivation

The benefit of the range syntax is obvious: when there are several contiguous cases, it is much more concise than listing each case individually.

While it would also be possible to handle case ranges using an `if` statement, this often requires splitting off some cases from the `switch`. This often results in a pattern like:

switch

(

c

)

{

case

'

+

'

:

consume_plus

(

)

;

break

;

case

'

=

'

:

consume_equals

(

)

;

break

;

default

:

{

if

(

c

>=

'

0

'

&&

c

<=

'

9

'

)

consume_digit

(

)

;

}

}

There is an obvious asymmetry here, decreasing readability.

In addition to the feature being generally useful in C++, as mentioned above, it is an existing C2y feature. Providing it to users would make it easier to port C code to C++ and vice versa. Historically, C++ has always provided the control flow constructs of C, although `defer` may likely result in divergence.

## Design

### Design strategy

The overall strategy is to copy the semantics of the C2y feature as accurately as possible. Among other things, this includes design decisions such as:

- `case 0 ... 1ll` is permitted. That is, mixing types in case ranges is allowed. However, just like for existing `case` labels, this does not permit narrowing conversions (to the type of the condition). A possible motivating example for mixing types in case ranges is: case 0 ... std::numeric_limits<std::uint8_t>::max(): Especially in generic code, it may be annoying if the types of both sides of the constant-range-expression have to be of the same type.
- Overlapping ranges are disallowed. That is, the `switch` needs to be able to select one `case` label unambiguously.
- Empty case ranges such as `case 0 ... -1` are valid, but recommended to be diagnosed.
- Both ends are inclusive. That is, `case 0 ... 1` includes the values 0 and 1, and can be expressed as the range [0, 1] in mathematical notation.

There is no obvious reason to deviate from the existing semantics of the C2y feature and of the C++ compiler extension; all these choices seem adequate.

A noteworthy quirk of the C2y feature is that a `case 0...9` is not valid because `0...9` is parsed as a pp-number from which no valid token can be formed, rather than two integers separated by an ellipsis. This problem cannot be fixed without altering the lexer, and having differences in lexing between C and C++ seems undesirable. It is recommended to always surround the `...` token with spaces, which works around the problem.

This quirk is also documented at [https://gcc.gnu.org/onlinedocs/gcc/Case-Ranges.html](https://gcc.gnu.org/onlinedocs/gcc/Case-Ranges.html).

### Scoped enumerations

One feature exclusive to the C++ compiler extension is the support for scoped enumerations, such as in:

enum

class

Status

{

added

,

removed

,

error_general

,

error_invalid_argument

,

}

;

void

handle

(

Status

s

)

{

switch

(

s

)

{

case

Status

::

added

...

Status

::

removed

:

break

;

case

Status

::

error

...

Status

::

error_invalid_argument

:

print_error

(

s

)

;

break

;

}

}

This part should also be standardized because it is useful. Many scoped enumerations organize enumerations into blocks, such as success statuses and error statuses, and case ranges allow for selecting such blocks.

The range check does not undergo overload resolution for `operator<`, but takes place in terms of the underlying type of the enumeration.

### Why not wait for pattern matching?

Pattern matching provides very similar functionality:

| P2688R4 | C2y |
| --- | --- |
| next_char match {
'x' => f();
let c if (c >= '0' && c <= '9') => g();
} | switch (next_char) {
case 'x': f(); break;
case '0' ... '9': g(); break;
} |

Nonetheless, the case ranges are worth considering for C++29 for a variety of reasons:

- Case ranges are more concise. To be fair, a similar `'0' ... '9'` match-case-pattern could also be added to pattern matching, which closes that gap.
- `case` can be used in ways that pattern matching doesn't allow, like being nested inside other statements or with fallthrough into subsequent cases. Such uses are rare, but do exist.
- Case ranges can be used in code that is meant to compile in both C and C++, which is unlikely to ever be the case for pattern matching.
- Similarly, case ranges make it easier to port from C code that uses them to C++ code, and vice versa.
- Standardizing case ranges legitimizes existing C++ code that relies on the GNU extension for case ranges by giving it behavior specified by the standard. This increases portability of old code bases that won't be modernized to use pattern matching.

### What about pack expansion case labels?

It may be worth considering a pack expansion `case` label, like:

template

<

auto

...

Args

>

void

f

(

int

i

)

{

switch

(

i

)

{

case

Args

...

:

break

;

}

}

However, this is not proposed, and is an entirely separate feature. The only thing it has in common with the proposed syntax is the `...`. Pack expansion `case` labels are also not strictly more general; this proposal offers `case 0 ... 1'000'000'000:`, and doing the same via pack expansion would require a pack well past any compiler limits.

Adding case ranges also does not make it impossible to add pack expansion cases later; pack expansions use `...` as a unary suffix operator, not as a binary operator.

If both features existed, the following `case` would be disambiguated as an unexpanded pack on the left side of a constant-range-expression:

case

Args

...

end

:

This is fine because interpreting `Args...` as a pack expansion would make the construct as a whole invalid. Therefore, no change in meaning takes place, whether pack expansions are supported or not.

In conclusion, pack expansion `case` labels are not proposed, and case ranges do not prevent such a feature from being added in the future.

## Implementation experience

Case ranges were first implemented in GCC 2.0 (1992) and Clang 1.0 (2007), albeit as a GNU extension, not as a standard C2y feature. Both GCC and Clang currently provide the feature as proposed in both C and C++ mode.

MSVC does not support case ranges.

## Wording

The changes are relative to [[N5032]](https://wg21%2elink/n5032).

### [cpp.predefined]

Add a feature-test macro to the table in [[cpp.predefined]](https://eel.is/c++draft/cpp.predefined) as follows:

__cpp_case_ranges

20

XXXXL

### [stmt.label]

Change [[stmt.label]](https://eel.is/c++draft/stmt.label) as follows:

A label can be added to a statement or used anywhere in a compound-statement.

**label:**
: attribute-specifier-seq<sub>opt</sub> identifier `:`
: attribute-specifier-seq<sub>opt</sub> `case` constant-expression `:`
: <ins>attribute-specifier-seq<sub>opt</sub> `case` constant-range-expression `:`</ins>
: attribute-specifier-seq<sub>opt</sub> `default` `:`
**labeled-statement:**
: label statement
**<ins>constant-range-expression:</ins>**
: <ins>constant-range-expression `...` constant-range-expression</ins>

[…]

### [stmt.switch]

Change [[stmt.switch] paragraph 2](https://eel.is/c++draft/stmt.switch#2) as follows:

2 If the condition is an expression, the value of the condition is the value of the expression; otherwise, it is the value of the decision variable. The value of the condition shall be of integral type, enumeration type, or class type. If of class type, the condition is contextually implicitly converted ([[conv]](https://eel.is/c++draft/conv)) to an integral or enumeration type. If the (possibly converted) type is subject to integral promotions ([[conv.prom]](https://eel.is/c++draft/conv.prom)), the condition is converted to the promoted type.

<ins>3</ins> Any statement within the `switch` statement can be labeled with one or more <del>case</del> <ins>`case`</ins> labels <del>as follows</del> <ins>of one of the forms</ins>:

: <ins>attribute-specifier-seq<sub>opt</sub></ins> case constant-expression `:`
: <ins>attribute-specifier-seq<sub>opt</sub> case constant-range-expression `:`</ins>

where the constant-expression <ins>of the first form and each constant-expression of the constant-range-expression of the second form</ins> shall be a converted constant expression ([[expr.const]](https://eel.is/c++draft/expr.const)) of the adjusted type of the <del>switch</del> <ins>`switch`</ins> condition. <ins> Let the value range of a `case` label L be: <ins>if L is of the first form, [x,x], where x is the converted value of the constant-expression;</ins> <ins>otherwise, [a,b], where a and b are the converted values of the first and second constant-expression of the constant-range-expression, respectively.</ins> No two of the <del>case constants in the same switch</del> <ins>value ranges of labels associated with the same `switch` statement</ins> shall <del>have the same value after conversion</del> <ins>overlap</ins>. </ins>

Attach an example to the previous paragraph (now paragraph 3):

[*Example*:

unsigned

int

i

=

0

;

switch

(

i

)

{

case

-

1

:

//

error: narrowing conversion from

-1

to

unsigned int

case

0

...

10

ull

:

//

OK

case

2

...

3

:

//

error: value range overlaps with that of previous label

}

— *end example*]

Immediately following [[stmt.switch] paragraph 2](https://eel.is/c++draft/stmt.switch#2) (now split into two paragraphs), insert a new paragraph:

*Recommended practice*: Implementations should emit a warning when the value range of a `case` label is empty.

This recommendation also exists in C2y.

Change [[stmt.switch] paragraph 3](https://eel.is/c++draft/stmt.switch#3) as follows:

There shall be at most one label of the form

: <ins>attribute-specifier-seq<sub>opt</sub></ins> `default` `:`

<del>within</del> <ins>associated with</ins> a `switch` statement.

Change [[stmt.switch] paragraph 4](https://eel.is/c++draft/stmt.switch#4) as follows:

Switch statements can be nested; a `case` or `default` label is <del>associated with</del> <ins>associated with</ins> the smallest <del>switch</del> <ins>`switch` statement</ins> enclosing it.

Change [[stmt.switch] paragraph 5](https://eel.is/c++draft/stmt.switch#5) as follows:

When the `switch` statement is executed, its condition is evaluated<del>. If one of the case constants has the same value as the condition, control is passed to the statement following the matched `case` label. If no case constant matches the condition, and if there is a `default` label, control passes to the statement labeled by the default label. If no case matches and if there is no `default` then none of the statements in the switch is executed. </del> <ins>, and control may be passed to one of the statements labeled by a label associated with the `switch` statement, selected as follows:</ins>

- <ins> If the value of the condition lies in the value range of one of the `case` labels, control is passed to the statement labeled by that label. If the value is of enumeration type `E`, determining whether it lies in the value range takes place by first explicitly converting ([[expr.type.conv]](https://eel.is/c++draft/expr.type.conv)) the value range boundaries and the value of the condition to the underlying type of `E`. </ins>
- <ins> Otherwise, if there is a `default` label, control is passed to the statement labeled by that label. </ins>
- <ins> Otherwise, control is not passed to any of the statements in the `switch` statement. </ins>

## References

[N3370]

Alex Celeste.

Case range expressions, v3.1

2024-10-01

https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3370.htm

[N5032]

Thomas Köppe.

Working Draft, Programming Languages — C++

2025-12-15

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/n5032.pdf
