---
title: "Case ranges"
document: P4040R1
date: 2026-15-07
audience: SG22
reply-to:
  - "Jan Schultke <janschultke@gmail.com>"
---

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



### Changes since R0

- Added §4.3. Mixing unscoped enumeration types
- Revised wording following CWG reflector review

## Introduction

In 2024, [[N3370]](https://www%2eopen-std%2eorg/jtc1/sc22/wg14/www/docs/n3370%2ehtm) added support for `case` ranges to C2y. For example, the following two `switch` statements are equivalent:

<!-- tomd:lossy-table -->

```cpp
switch (next_char) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9': // ...
}
```

```cpp
switch (next_char) {
case '0' ... '9': // ...
}
```

The behavior here is obvious; `case '0' ... '9':` specifies ten cases in bulk.

This feature is already implemented in GCC and Clang, not just for C but also for C++. Since it is useful and widely supported, we should standardize existing practice and make it available to users in standard 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:

```cpp
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.
- Mixing different unscoped enumeration types is permitted.
- 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:

```cpp
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.

### Mixing unscoped enumeration types

Currently, the following construct is valid in both C2y and C++26:

```cpp
enum A { X };
enum B { Y };
void f() {
switch (X) {
case Y:;
}
}
```

In C++, this is because `Y` is a converted constant expression ([[stmt.switch]](https://eel.is/c++draft/stmt.switch)) of type `A`, and the conversion from values of type `B` to `A` is well-defined and not narrowing ([[dcl.init.list] definition of "conversion,narrowing"](https://eel.is/c++draft/dcl.init.list#def:conversion,narrowing)).

> At the time of writing, out of GCC, Clang, and MSVC, only Clang diagnoses this as a `-Wenum-compare-switch` warning, and Clang only does so in C++ mode.

Consequently, this paper permits the following (except the code would be invalid due to overlapping ranges):

```cpp
switch (X) {
case Y:; // OK, pre-existing mismatch between condition and label type
case Y ... Y:; // OK, new mismatch between condition and label type
case X ... Y:; // OK, mixing different unscoped enumerations a case range
}
```

It could be argued that permitting this mixing is surprising. Also, [[P2864R2]](https://wg21%2elink/p2864r2) removed the (deprecated in C++20) ability to mix different enumeration types in comparisons (or generally, in usual arithmetic conversions), and disallowing enumeration mixing in `switch` statements would be consistent with that design.

#### Arguments against restrictions in this paper

However, the current behavior has existed in the C++ extension for many years and is the current C2y behavior, so any change may break existing code and create incompatibility with C. Making that change is also not necessary right now; the proposed feature is an ancient extension, so we make a breaking change whether we restrict the behavior now or in a few years.

Making the behavior more restrictive should also be done holistically: if writing `X ... Y` is disallowed, then `case Y:` should also be disallowed when there is a mismatch with the condition type, but that is an entirely separate and pre-existing issue for a separate paper.

#### Deprecate mixing enumeration types

A compromise worth considering is to allow mixing enumeration types now, but to deprecate it on arrival. Deprecating new features is unconventional, but seems appropriate considering that the new feature is pre-existing as an extension, so any changes to it should be subject to deprecation just like existing standard features.

### Why not wait for pattern matching?

Pattern matching provides very similar functionality:

<!-- tomd:lossy-table -->

```cpp
next_char match {
'x' => f();
let c if (c >= '0' && c <= '9') => g();
}
```

```cpp
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:

```cpp
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:
> 
> ```cpp
> 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
__cpp_case_ranges 20XXXXL
```

### [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 integer value of a value v be v if v is of integral type other than `bool`, and otherwise the value obtained by converting v to the underlying type of v. Let the integer range of a `case` label L be: <ins>if L is of the first form, [x,x], where x is the integer value of the converted value of the constant-expression;</ins> <ins>otherwise, [a,b], where a and b are the integer values of 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*:

```cpp
unsigned int i = 0;
switch (i) {
case -1: // error: narrowing conversion from -1 to unsigned int
case 0 ... 10ull: // OK
case 10 ... 15: // error: integer 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 integer 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 with a label associated with the `switch` statement, selected as follows:</ins>

- <ins> If the integer value of the value of the condition lies in the integer range of one of the `case` labels, control is passed to the statement labeled with that label. </ins>
- <ins> Otherwise, if there is a `default` label, control is passed to the statement labeled with the `default` 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

[P2864R2]

Alisdair Meredith.

Remove Deprecated Arithmetic Conversion on Enumerations From C++26

2023-11-10

https://www.open-std.org/JTC1/SC22/WG21/docs/papers/2023/p2864r2.pdf
