1. Changelog
1.1. Revision 1 - April 12sth, 2022
-
More directly specify the algorithm for selecting the types of enumeration constants, both after and midway through the definition of an enumeration.
-
Move all of the specification for the new algorithm into §6.7.2.2 in the § 5.2.2 Modify Section §6.7.2.2 Enumeration constants.
-
Add more rationale in § 4.1 Using the Enumerators Midway in the Definition List for the problems found in current implementation extensions.
1.2. Revision 0 - January 1st, 2022
-
Initial release 🎉!
2. Introduction and Motivation
C always designates
as the type for the enumerators of its enumerations, but it’s entirely unspecified what the (underlying compatible) type for the
will end up being. These constants (and the initializers for those constants) must fit within an
, otherwise it is a constraint violation. For decades, compilers have been silently providing extensions in their default build modes for enumerations to be larger than
, even if
and friends always detects the type of such an enumerator to be
. It is problematic to only have enumerators which are
, especially since it is only guaranteed to be 16-bits wide. Portability breaks happen between normal 32-bit
environments like typical GCC and Clang x86 platforms vs. 16-bit
environments like SDCC microcontroller targets, which is not desirable.
This proposal provides for enumerations with enumerators of values greater than
and smaller than
to have enumerators that are of a different type than
, allowing the underlying type and the enumeration constants themselves to be of a different type. It does not change behavior for any enumeration constants which were within the
range already.
3. Prior Art
The design of this feature is to enable what has been existing practice on implementations for a long time now, including GCC, SDCC, Clang, and several other compilers. Compilers have allowed for values greater than
and values less than
for a long time in their default compilation modes. We capture this as part of the design discussion below, for how we structure these proposed changes to the C Standard.
4. Design
This is a very small change that only makes previously ill-formed code now well-formed. It does not provide any other guarantees from the new wording besides allowing constants larger than
to be used with enumerations. Better enumeration types and values are better left with the sister paper on Enhanced Enumerations.
More specifically:
-
values for enumerators that are outside of the range
are allowed and change the type of the enumerators from[ INT_MIN , INT_MAX ]
;int -
and, the underlying type for enumerations with such types may be larger than
.int
Particularly, this code:
enum a { a0 = 0xFFFFFFFFFFFFFFFFULL }; int main () { return _Generic ( a0 , unsigned long long : 0 , int : 1 , default : 2 ); }
Should produce a value of
on a normal implementations (but can give other values, so long as the underlying type is big enough to fit a number (264 - 1)). It shall also not produce a diagnostic on even the most strict implementations.
4.1. Using the Enumerators Midway in the Definition List
Given this following code snippet:
#include <limits.h>#define GET_TYPE_INT(x) _Generic(x, \ char: 1,\ unsigned char: 2,\ signed char: 3,\ short: 4,\ unsigned short: 5,\ int: 6,\ unsigned int: 7,\ long: 8,\ unsigned long: 9,\ long long: 10,\ unsigned long long: 11,\ default: 0xFF\ )\ enum x { a = INT_MAX , b = ULLONG_MAX , a_type = GET_TYPE_INT ( a ), b_type = GET_TYPE_INT ( b ) }; #include <stdio.h>int main () { printf ( "sizeof(long)=%d \n " , ( int ) sizeof ( long )); printf ( "sizeof(long long)=%d \n " , ( int ) sizeof ( long long )); printf ( "a_type=%d \n " , ( int ) a_type ); printf ( "b_type=%d \n " , ( int ) b_type ); printf ( "GET_TYPE_INT(a), outside=%d \n " , ( int ) GET_TYPE_INT ( a )); printf ( "GET_TYPE_INT(b), outside=%d \n " , ( int ) GET_TYPE_INT ( b )); return 0 ; }
Compilers are not consistent, depending on how far with extensions they like to go. Anyone who was depending on a specific type was not relying on either (a) compilable C code, according to the C standard, or (b) widely-existing cross-platform C code, according to what implementation extensions do. Therefore, we attempt to enshrine the best of the available existing practice and improve the status quo.
4.1.1. Midway Type: Whatever the Compiler Chooses
During the definition of an enumeration type, the enumeration constants themselves have whatever type the enumeration wants if they are not representable by
. This is as consistent as we can be for existing code that relies on using existing enumeration constants at definition time.
4.1.2. Final Type: the Enumerated Type
After the enumeration type’s definition is completed (after the
), the enumeration constants have the enumeration type (but only when all the constants are not representable by
). The reason we want to specify it this way is because implementations are wildly varying on how they handle this today in their extensions, with no clear consensus on how it is done. That is, using existing extensions today in various compilers, adding 3 extra lines to modify the snippet from the up-level section:
#include <limits.h>#define GET_TYPE_INT(x) _Generic(x, \ char: 1,\ unsigned char: 2,\ signed char: 3,\ short: 4,\ unsigned short: 5,\ int: 6,\ unsigned int: 7,\ long: 8,\ unsigned long: 9,\ long long: 10,\ unsigned long long: 11,\ default: 0xFF\ )\ enum x { a = INT_MAX , b = ULLONG_MAX , a_type = GET_TYPE_INT ( a ), b_type = GET_TYPE_INT ( b ) }; extern enum x e_a ; extern __typeof ( b ) e_a ; extern __typeof ( a ) e_a ; // …
results in various failures on today’s implementations. This is because
and
are of different types (
is an
and not compatible with
or
, since those are both compatible with
or
depending on the selection done by the implementation). We feel it is worthwhile to clarify this and make it more consistent.
There is no way to make such code portable, as it was (A) already using an extension that was not standardized until before C23 and (B) already relied in an implementation detail that could change between implementations, but also within a given implementation (e.g.,
). The above code is utterly non-portable, and outside of what we can possibly concern ourselves with when trying to provide at least some degree of standardized behavior. We can fix this by providing a consistent choice of the underlying integer type of
for the integer constants when used after the closing brace of
. We expect this to be the best possible and most consistent choice for enumerations going forward.
5. Proposed Wording
The following wording is relative to N2731.
5.1. Intent
The intent of the wording is to provide the ability to express enumerations with the underlying type present. In particular:
-
enumerations without an underlying type can have enumerators initialized with integer constant expressions whose type is
or some implementation-defined type capable of representing the constant expression’s value;int -
bit-precise types cannot be used for enumerations without an underlying type;
-
the type of the enumeration constants during definition may differ from when after the enumeration is completed; and
-
it is an error (constraint violation) to make an enumeration with a range of values that cannot be represented as any single signed or unsigned type.
5.2. Proposed Specification
5.2.1. Modify Section §6.4.4.3 Enumeration constants
6.4.4.3 Enumeration constantsSyntaxenumeration-constant:
identifier
SemanticsAn identifier declared as an enumeration constant for an enumeration has
typethe compatible integer type of the enumeration, as defined in 6.7.2.2.
int Forward references: enumeration specifiers (6.7.2.2).
5.2.2. Modify Section §6.7.2.2 Enumeration constants
6.7.2.2 Enumeration specifiersSyntaxenum-specifier:
enum attribute-specifier-sequenceopt identifieropt { enumerator-list }
enum attribute-specifier-sequenceopt identifieropt { enumerator-list , }
enum identifier
enumerator-list:
enumerator
enumerator-list , enumerator
enumerator:
enumeration-constant attribute-specifier-sequenceopt
enumeration-constant attribute-specifier-sequenceopt = constant-expression
ConstraintsThe expression that defines the value of an enumeration constant shall be an integer constant expression.
that has a value representable as anFor all the integer constant expressions which make up the values of the enumeration constant, there shall be an implementation-defined signed or unsigned integer type (excluding the bit-precise integer types) capable of representing all of the values..
int SemanticsThe identifiers in an enumerator list are declared as constants and may appear wherever such are permitted.133) An enumerator with = defines its enumeration constant as the value of the constant expression. If the first enumerator has no =, the value of its enumeration constant is 0. Each subsequent enumerator with no = defines its enumeration constant as the value of the constant expression obtained by adding 1 to the value of the previous enumeration constant. (The use of enumerators with = may produce enumeration constants with values that duplicate other values in the same enumeration.) The enumerators of an enumeration are also known as its members.
During the processing of each enumeration constant in the enumerator list, the type of the enumeration constant shall be:
—
, if there are no previous enumeration constants in the enumerator list and no explicit = with a defining integer constant expression; or,
int —
, if given explicitly with = and the value of the integer constant expression is representable by an
int ; or,
int — the type of the integer constant expression, if given explicitly with = and if the value of the integer constant expression is not representable by
; or,
int — the type of the value from last enumeration constant with
added to it. If such an integer constant expression would overflow the value of the previous enumeration constant from the addition of
1 , the type takes on either: a suitably sized signed integer type (excluding the bit-precise signed integer types), or a suitably sized unsigned integer type (excluding the bit-precise unsigned integer types), capable of representing the value of the previous enumeration constant plus
1 . A signed integer type is chosen if the previous enumeration constant being added is of signed integer type. An unsigned integer type is chosen if the previous enumeration constant is of unsigned integer type.
1 Each enumerated type shall be compatible with
, a signed integer type, or an unsigned integer type (excluding the bit-precise integer types) . The choice of type is implementation-defined139), but shall be capable of representing the values of all the members of the enumeration.
char The enumerated type is incomplete until immediately after the } that terminates the list of enumerator declarations, and complete thereafter. The type of all the members of the enumeration upon completion is:
—
if all the values of the enumeration are representable as an
int ; or,
int — the enumerated type.FN0✨)
138)Thus, the identifiers of enumeration constants declared in the same scope are all required to be distinct from each other and from other identifiers declared in ordinary declarators.139)An implementation can delay the choice of which integer type until all enumeration constants have been seen.FN0✨)The type selected during processing of the enumerator list (before completion) of the enumeration may be incompatible from the type selected after the enumeration is completed.
5.2.3. Add implementation-defined enumeration behavior to Annex J
6. Acknowledgements
Thanks to:
-
Aaron Ballman for help with the initial drafting;
-
Aaron Ballman, Aaron Bachmann, Jens Gustedt & Joseph Myers for questions, suggestions and offline discussion;
-
Robert Seacord for editing suggestions; and,
-
Clive Pygott for the initial draft of this paper.
We hope this paper serves you all well.