1. Revision History
1.1. Revision 0 - May 30th, 2025
-
Initial release. ✨
2. Introduction & Motivation
During the WG14 Minneapolis 2024 meeting, a long-standing existing practice was accepted into C2y for denoting a range of values to
on:
switch ( n ) { case 1 ... 10 : something (); break ; }
This is good, and synchronizes the standard with widespread current implementations and extensions. However, there is a distinct problem in that
denotes a fully inclusive range, that counts for both 1 and 10. This leads to unfortunate syntax and behaviors to make a range of 1 element, or a range of no elements:
switch ( n ) { case 1 ... 10 : something (); break ; case 11 ... 11 : something (); break ; case 21 ... 12 : not_correct (); break ; }
The second is a case range of 1 element:
. The third is an empty case range: it puts the high value first and the low value second. Unfortunately, this is error-prone and problematic because it’s a common user error: accidentally typing the wrong value first or getting closely-related values incorrect is a thing that can happen very often.
This problem with the current, existing case range design supported in C compilers resulted in the discussion on Monday, September 30th during the WG14 meeting where it was discussed in what ways this could be mitigated. The ultimate problem is that it was impossible to provide a Constraint for accidentally swapping the High and Low values of the case range expression because it was a relied-upon means of creating an empty range through Macro Usage:
#ifndef NUM_APPLES /* default value */ #define NUM_APPLES 5 #endif switch ( apple_val ) { case 0 ... NUM_APPLES : /* do something */ break ; } // ...
Part of this idiom is that users can do
and that will "turn off" everything in the
portion of the
’s
label. Therefore, the recommendation that was cultivated during the Minneapolis meeting was "do not warn if it’s a macro because it was probably intentional, otherwise warn because it is likely a mistake". Briefly ignoring non-fused implementation concerns (separated preprocessor vs. front-end with the front-end generating errors for things it does not understand may or may not be macro values), there is a much heavier concern that the moment one uses macros, implementations may stop warning on things that are mistakes. Similarly, this leads to other questions: what if it’s a
variable, does that silence warnings? A
expression that generates a constant value? There’s many different ways that this could go wrong.
The better way to handle this is by having it be a Constraint (i.e., typically an error in most implementations). The use cases above are valid and are part of existing practice, so this paper instead proposes a different syntax for "half-open ranges", as has been done in other languages that have encountered this same problem and moved to get around this issue.
3. Design
We cannot replace the existing syntax as it has strong existing practice, so we provide an alternative syntax to give us the power that we need. The syntax chosen in this proposal is
, which is meant to clearly illustrate half-open/half-inclusive ("one less than the end") ranges. The reasons for choosing this syntax are simple:
-
it is a token sequence not present in C or C++ and therefore can be safely integrated in most languages;
-
it is not valid as part of any valid syntactic form of of parseable language construct in C or C++;
-
it is visually similar to
while being distinct enough to not be missed like just having two dots...
;.. -
and, the less than symbol matches nicely with the idea that it is "from
up to (but not including)low - value
", similar to for loop syntax.high - value
For these reasons, we settled with
. This provides us with a way of accessing a constraint violation without changing the meaning of existing code. It allows us to properly diagnose the following problem:
extern int n ; extern void f (); int main () { switch ( n ) { case 8. . .7 : // mistake: probably diagnosed! f (); break ; } switch ( n ) { case 8. . < 7 : // mistake: constraint violation! f (); break ; } return 0 ; }
and ALSO allows us to diagnose it even if the name comes from a macro or a
variable, which provides a proper out from the way thorny diagnostics implementers were brainstorming at the Minneapolis 2024 meeting:
extern int n ; extern void f (); #define LO 0 #define HI 50 constexpr int lo = 60 ; constexpr int hi = 99 ; int main () { switch ( n ) { case HI ... LO -1 : // mistake: not diagnosed // Minneapolis 2024 recommendation: DO NOT diagnose!! case hi ... lo -1 : // mistake: not diagnosed // Minneapolis 2024 recommendation: DO NOT diagnose!! f (); break ; } switch ( n ) { case HI .. < LO : // mistake: constraint violated! case hi .. < lo : // mistake: constraint violated! f (); break ; } return 0 ; }
3.1. Syntax
We chose
because it is visually indicative of "less than" and works fairly well as an individual token recognizable in the preprocessor with no parsing ambiguities in C and C++. We also note that a wide variety of languages also have came to the same conclusion as this paper, that having both a closed range and an open range specifier in the language is necessary for both ease-of-use and intent-specifying, diagnostic-capable reasoning. Some languages:
Language | Closed Range | Half-open |
---|---|---|
C/C++ |
| (This Proposal)
|
Swift |
|
|
Rust |
|
|
Perl |
| ⛔️ |
Raku |
|
|
Kotlin |
|
|
Ruby |
|
|
Odin |
|
|
Python |
|
|
4. Existing Practice
Currently, no C compiler implementers the additional case range syntax. We are proposing this purely to mitigate the existing problem with the case ranges and to allow for better error checking for existing compilers based on the previous extension.
Previously, [n3370]'s older iterations had suggestions for a half-open range. It was dropped for expedience purposes of standardization. However, we believe it would still be a good idea.
5. Wording
The following wording is against the latest draft of the C standard.
5.1. Modify §6.6.2 "Constant range expressions"
6.6.2Constant Range ExpressionsSyntaxconstant-range-expression:
constant-expression
constant-expression
... - constant-expression
constant-expression
.. < Description...
ConstraintsThe constant expressions shall be integer constant expressions. For a half-open range, the first constant expression shall be less than or equal to the second constant expression.
SemanticsThe values described by the
operator form a closed range, which contains all integer values in sequence starting from and including the first, low value, up to and including the second, high value.
... 123)The values described by the
operator form a half-open range, which contains all integer values in sequence starting from and including the first, low value, up to but not including the second, high value.
.. < 123) A range is not itself usable as a value and therefore does not have any specific type or representation, or perform any type conversion.IfFor closed ranges, if the arithmetic value of the first constant expression is greater than theone of the secondarithmetic value of the second , the range described by the constant range expression is empty. For half-open ranges, if the arithmetic value of the first constant expression is equal to the arithmetic value of the second, the range described by the constant range expression is empty.NOTE A range is not itself usable as a value and therefore does not have any specific type or representation, or perform any type conversion.
Recommended PracticeImplementations are encouraged to emit a diagnostic message when a range expression results in a closed range that is empty.
...
EXAMPLE 2
Because a range expression describes a closed range, it is possible to match past-the-end values such as the size of an array:Range expressions which describe closed ranges allow matching past-the-end values of a sufficiently-sized array, while range expressions which describe half-open ranges will not reference past the end of a sufficiently sized array:constexpr const int N = 42 ; int arr [ N ]; switch ( i ) { case 0 ... N : // matches the past-the-end range of arr f ( arr [ i ]); // not OK, will dereference arr[N] g ( & arr [ i ]); // may be OK depending on purpose break ; } switch ( i ) { case 0 ... N - 1 : // only matches the valid element range of arr f ( arr [ i ]); // OK break ; } switch ( i ) { case 0 .. < N : // only matches the valid element range of arr f ( arr [ i ]); // OK break ; } ...EXAMPLE 4 Half-open ranges can provide better constraints for specific scenarios, such as working naturally with values that are not valid when accidentally flipped:
extern int n ; extern void f ( int val ); constexpr int lo = -40 ; constexpr int hi = 50 ; int main () { switch ( n ) { case hi ... lo -1 : // mistake: no constraint violation f ( n ); break ; } switch ( n ) { case hi .. < lo : // mistake: constraint violation f ( n ); break ; } return 0 ; }