Bit-precise integers
- Document number:
- D3666R0
- Date:
2025-09-01 - Audience:
- SG6, SG22, EWG
- Project:
- ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21
- Reply-to:
- Jan Schultke <janschultke@gmail.com>
- GitHub Issue:
- wg21.link/P3666/github
- Source:
- github.com/Eisenwave/cpp-proposals/blob/master/src/bitint.cow
Contents
Revision history
Introduction
Motivation
Computation beyond 64 bits
C ABI compatibility
Resolving issues with the current integer type system
Design discussion
Why not make it a library type?
Full C compatibility requires fundamental types
Tiny integers are useful in C++
Special deduction rules
Quality of implementation requires a fundamental type
Naming
Why no _t
suffix?
Why the keyword spelling?
Should it be optional? Is this too hard to implement?
_BitInt ( 1 )
Degree of library support
Implementation experience
Impact on the standard
Wording
Core
[lex.icon]
[basic.fundamental]
[conv.rank]
[conv.prom]
[dcl.type.simple]
[cpp.predefined]
Library
[cstdint.syn]
[climits.syn]
References
1. Revision history
This is the first revision.
2. Introduction
[N2763] introduced the
set of types to the C23 standard,
and [N2775] further enhanced this feature with literal suffixes.
For example, this feature may be used as follows:
In short, the behavior of these bit-precise integers is as follows:
-
No integer promotion to
takes place.int - Mixed-signedness comparisons, implicit conversions, and other permissive feature are supported.
-
They have lower conversion rank than standard integers,
so an operation between
and_BitInt ( 8 )
yieldsint
, as does an operation withint
where_BitInt ( N )
is the width ofN
. They only have greater conversion rank when their width is greater.int
3. Motivation
3.1. Computation beyond 64 bits
Computation beyond 64-bit bits, such as with 128-bits is immensely useful. A large amount of motivation for 128-bit computation can be found in [P3140R0]. Computations in cryptography, such as for RSA require even 4096-bit integers.
3.2. C ABI compatibility
C++ currently has no portable way to call C functions such as:
While one could rely on the ABI of
and
to be identical in the first overload,
there certainly is no way to portably invoke the second overload.
This compatibility problem is not a hypothetical concern either; it is an urgent problem.
There are already targets with
supported by major compilers,
and used by C developers:
Compiler | Targets | Languages | |
---|---|---|---|
clang 16+ |
|
all | C & C++ |
GCC 14+ |
|
64-bit only | C |
MSVC | ❌ | ❌ | ❌ |
3.3. Resolving issues with the current integer type system
as standardized in C solves multiple issues that
the standard integers (
etc.) have.
Firstly, integer promotion can result in unexpected signedness changes.
is an alias for
.
Surprisingly,
is not
because
is promoted to
in
,
so the subsequent right-shift by
shifts one set bit into
from the left.
Even more surprisingly, if we had used
instead of
for
,
would be
,
despite our code seemingly using only unsigned integers.
This design is terribly confusing and makes it hard to write bit manipulation
for integers narrower than
.
Lastly, there is no portable way to use an integer with exactly 32 bits.
and
may be wider,
and
is an optional type alias which only exists if such an integer type
has no padding bits.
While most users can use
without much issue,
its optionality is a problem for use in the standard library and other ultra-portable libraries.
4. Design discussion
The overall design strategy is as follows:
- The proposal is a C compatibility proposal first and foremost. Whenever possible, we match the behavior of the C type.
-
The goal is to deliver a minimal viable product (MVP)
which can be integrated into the standard as quickly as possible.
This gives us plenty of time to add standard library support wherever desirable over time,
as well as other convenience features surrounding
._BitInt
4.1. Why not make it a library type?
[P3639R0] explored in detail whether to make it a fundamental type or a library type.
Furthermore, feedback given by SG22 and EWG was to make it a fundamental type, not a library type.
This boils down to two plausible designs
(assuming
is already supported by the compiler), shown below.
𝔽 – Fundamental type | 𝕃 – Library type |
---|---|
The reasons why we should prefer the left side are described in the following subsections.
4.1.1. Full C compatibility requires fundamental types
in C can be used as the type of a bit-field, among other places:
Since C++ does not support the use of class types in bit-fields,
such a
could not be passed from C++ to a C API.
A developer would face severe difficulties
when porting C code which makes use of these capabilities to C++
and if bit-precise integers were a class type in C++.
4.1.2. Tiny integers are useful in C++
In some cases, tiny
's may be useful as the underlying type of an enumeration:
By using
rather than
,
every possible value has an enumerator.
If we used e.g.
instead,
there would be 252 other possible values that simply have no name,
and this may be detrimental to compiler optimization of
statements etc.
4.1.3. Special deduction rules
While this proposal focuses on the minimal viable product (MVP), a possible future extension would be new deduction rules allowing the following code:
Being able to make such a call to
is immensely useful because it would allow
for defining a single function template which may be called with every possible
signed integer type,
while only producing a single template instantiation
for
,
, and
,
as long as those three have the same width.
The prospect of being able to write bit manipulation utilities that simply accept
is quite appealing.
If
was a class type,
this would not work because template argument deduction would fail,
even if there existed an implicit conversion sequence from
to
.
These kinds of deduction rules may be shutting the door on this mechanism forever.
4.1.4. Quality of implementation requires a fundamental type
While a library type
gives the implementation
the option to provide no builtin support for bit-precise integers,
to achieve high-quality codegen,
a fundamental type is inevitably needed anyway.
If so,
is arguably adding pointless bloat.
For example, when an integer division has a constant divisor, like
,
it can be optimized to a fixed-point multiplication,
which is much cheaper.
Performing such an optimization requires the compiler to be aware that a division is taking place,
and this fact is lost when division is implemented in software,
as a loop which expands to hundreds of IR instructions.
"Frontend awareness" of these operations is also necessary to provide compiler warnings
when a division by zero or a bit-shift with undefined behavior is spotted.
Use of
on e.g.
cannot be used to achieve this
because numerics code needs to have no hardened preconditions and no contracts,
for performance reasons.
Another workaround would be an ever-growing set of implementation-specific attributes,
but at that point, we may as well make it fundamental.
4.2. Naming
The approach is to expose bit-precise integers via two alias templates:
The goal is to have a spelling reminiscent of the C
spelling.
There are no clear problems with it,
so it is the obvious candidate.
4.2.1. Why no _t
suffix?
While the
suffix would be conventional for simple type aliases
such as
,
there is no clear precedent for alias templates.
There are alias templates such as
without any
or
suffix,
but "type trait wrappers" such as
which have a
suffix.
The
suffix does not add any clear benefit,
adds verbosity,
and distances the name from the C spelling
.
Brevity is important here because
is expected to be a commonly spelled type.
A function doing some bit manipulation could use this name numerous times.
4.2.2. Why the keyword spelling?
I also propose to standardize the keyword spelling
and
.
While a similar approach could be taken as with the
compatibility macro,
macros cannot be exported from modules,
and macros needlessly complicate the problem compared to a keyword.
The objections to a keyword spelling are that it's not really necessary, or that it "bifurcates" the language by having two spellings for the same thing, or that those ugly C keywords should not exist in C++. Ultimately, it's not the job of WG21 to police code style; both spellings have a right to exist:
-
The
alias template fits in aesthetically with the rest of the language, and conveys clearly (via "pointy brackets") that the given width is a constant expression.std :: bit_int -
The
spelling is useful for writing C/C++-interoperable code, and C compatibility is an important design goal. Furthermore, the spelling is going to exist whether that would be a compatibility macro or a keyword, and since there is no clear technical benefit to a macro, it should be a keyword._BitInt
Furthermore, to enable C compatibility, all of the spellings
,
and
need to be valid.
This goes far beyond the capabilities that a compatibility macro like
can provide without language support.
The most likely wording path would be to create an exposition-only
spelling to define
etc., which makes our users beg the question:
Why is there an compatibility macro for an exposition-only keyword spelling?! Why are we making everything more complicated by not just copying the keyword from C?! Why is this exposition-only when it's clearly useful for users to spell?!
keyword spelling as a compiler extensions,
so this is standardizing existing practice.
4.3. Should it be optional? Is this too hard to implement?
As in C,
is only required to support
of at least
, which has a minimum of
.
This makes
a semi-optional feature,
and it is reasonable to mandate its existence, even in freestanding platforms.
Of course, this has the catch that
may be completely useless
for tasks like 128-bit computation.
As unfortunate as that is, the MVP should include no more than C actually mandates.
Mandating a greater minimum width could be done in a future proposal.
4.4. _BitInt ( 1 )
C23 does not permit
but does permit
.
This is an irregularity that could make generic programming harder in C++.
Whether
should be permitted in C++ depends somewhat on
the outcome of [N3644],
a WG14 proposal which makes
valid.
That proposal also contains some practical motivation for why
a single-bit should be permitted.
was allowed,
it would be able to represent the values
and
.
4.5. Degree of library support
As previously stated, the overall strategy of this proposal is to ship an MVP. There are three categories of library features that deal with integer types:
-
Features which are stated to support any integer type,
such as
or<bit>
. These should generally support bit-precise integers too, since there's typically no reason to single out bit-precise integers, and support would be useful.<simd> -
Functions originating from C with support for bit-precise integer types,
such as
. The C functions typically support only bit-precise integers with the same width as some standard integer type. This restriction is unmotivated in C++ since a function template can easily cover any bit-precise integer rather than having a finite set of options covered by C'sstd :: sqrt
selection, so we introduce a function template overload instead._Generic -
Features which explicitly support only standard integers,
such as
or thestd :: to_string
constructors. These should be addressed by follow-up proposals because they are not part of the MVP.std :: bitset
5. Implementation experience
, formerly known as
, has been a compiler extension
in Clang for several years now.
The core language changes are essentially standardizing that compiler extension.
6. Impact on the standard
7. Wording
The following changes are relative to [N5014].
7.1. Core
[lex.icon]
In [lex.icon], change the grammar as follows:
integer-suffix :- unsigned-suffix long-suffixopt
- unsigned-suffix long-long-suffixopt
- unsigned-suffix size-suffixopt
- long-suffix unsigned-suffixopt
- long-long-suffix unsigned-suffixopt
- unsigned-suffixopt bit-precise-int-suffix
- bit-precise-int-suffix unsigned-suffixopt
unsigned-suffix : one ofu U
long-suffix : one ofl L
long-long-suffix : one ofll LL
size-suffix : one ofz Z
bit-precise-int-suffix : one ofwb WB
Change table [tab:lex.icon.type] as follows:
[…] | […] | […] |
Both or and or
|
|
|
or
|
of minimal width
so that the value of the literal can be represented by the type.
|
of minimal width
so that the value of the literal can be represented by the type.
|
Both or and or
|
of minimal width
so that the value of the literal can be represented by the type.
|
of minimal width
so that the value of the literal can be represented by the type.
|
Change [lex.icon] paragraph 4 as follows:
Except for
[Note:
An
or
suffix
is ill-formed if it cannot be represented by
.
An
or
suffix
is ill-formed if it cannot be represented by any
because the necessary width is greater than
([climits.syn]).
— end note]
[basic.fundamental]
Change [basic.fundamental] paragraph 1 as follows:
There are five standard signed integer types:
,
,
,
, and
.
In this list,
each type provides at least as much storage as those
preceding it in the list.
There is also a distinct bit-precise signed integer type
for each ([climits.syn]).
There may also be implementation-defined
extended signed integer types.
The standard, bit-precise, and extended signed integer types are collectively called
signed integer types.
The range of representable values for a signed integer type is
to
(inclusive),
where is called the width of the type.
of width
[Note:
Plain
s are intended to have
the natural width suggested by the architecture of the execution environment;
the other signed integer types are provided to meet special needs.
— end note]
Change [basic.fundamental] paragraph 2 as follows:
For each of the standard signed integer types,
there exists a corresponding (but different)
standard unsigned integer type:
,
,
,
, and
.
For each bit-precise signed integer type
,
there exists a corresponding bit-precise unsigned integer type
of width
.
Additionally, there exists the type
of width
.
of width 1Likewise, for For each of the extended signed integer types,
there exists a corresponding extended unsigned integer type.
The standard, bit-precise, and extended unsigned integer types
are collectively called unsigned integer types.
An unsigned integer type has the same width
as the corresponding signed integer type.
The range of representable values for the unsigned type is
to
(inclusive);
arithmetic for the unsigned type is performed modulo .
[Note: Unsigned arithmetic does not overflow. Overflow for signed arithmetic yields undefined behavior ([expr.pre]). — end note]
Change [basic.fundamental] paragraph 5 as follows:
[…]
The standard signed integer types and standard unsigned integer types
are collectively called the standard integer types, and the
. The bit-precise signed integer types and bit-precise unsigned integer types
are collectively called the bit-precise integer types. The
extended signed integer types and extended
unsigned integer types are collectively called the
extended integer types.
[conv.rank]
Change [conv.rank] paragraph 1 as follows:
Every integer type has an integer conversion rank defined as follows:
-
No two signed integer types other than
andchar
(ifsigned char char is signed) have the same rank, even if they have the same representation. - The rank of a signed integer type is greater than the rank of any signed integer type with a smaller width.
-
The rank of
is greater than the rank oflong long int
, which is greater than the rank oflong int
, which is greater than the rank ofint
, which is greater than the rank ofshort int
.signed char - The rank of any unsigned integer type equals the rank of the corresponding signed integer type.
- The rank of any standard integer type is greater than the rank of any bit-precise integer type with the same width and of any extended integer type with the same width.
-
The rank of
equals the rank ofchar
andsigned char
.unsigned char -
The rank of
is less than the rank of all standard integer types.bool -
The ranks of
,char8_t
,char16_t
, andchar32_t
equal the ranks of their underlying types ([basic.fundamental]).wchar_t - The rank of any extended signed integer type relative to another extended signed integer type with the same width and relative to a bit-precise signed integer type with the same width is implementation-defined, but still subject to the other rules for determining the integer conversion rank.
-
For all integer types
,T1
, andT2
, ifT3
has greater rank thanT1
andT2
has greater rank thanT2
, thenT3
has greater rank thanT1
.T3
[Note: The integer conversion rank is used in the definition of the integral promotions ([conv.prom]) and the usual arithmetic conversions ([expr.arith.conv]). — end note]
[conv.prom]
Change [conv.prom] paragraph 2 as follows:
A prvalue that
- is not a converted bit-field
and, -
has
ana standard or extended integer type other than
,bool
,char8_t
,char16_t
, orchar32_t
, andwchar_t -
whose integer conversion rank ([conv.rank])
is less than the rank of
int
can be converted to
a prvalue of type
if
can represent all the values of the source type;
otherwise, the source prvalue can be converted to
a prvalue of type
.
[dcl.type.simple]
Change [dcl.type.simple] paragraph 1 as follows:
The simple type specifiers are
simple-type-specifier :- nested-name-specifieropt type-name
- nested-name-specifier
simple-template-idtemplate - computed-type-specifier
- placeholder-type-specifier
- nested-name-specifieropt template-name
char char8_t char16_t char32_t wchar_t bool short int long signed unsigned float double void type-name :- class-name
- enum-name
- typedef-name
computed-type-specifier :- decltype-specifier
- pack-index-specifier
- splice-type-specifier
- bit-precise-int-type-specifier
bit-precise-int-type-specifier :_BitInt
constant-expression( )
Change table [tab:dcl.type.simple] as follows:
Specifier(s) | Type |
---|---|
the type named | |
the type as defined in [temp.names] | |
the type as defined in [dcl.type.decltype] | |
the type as defined in [dcl.type.pack.index] | |
the type as defined in [dcl.spec.auto] | |
the type as defined in [dcl.type.class.deduct] | |
the type as defined in [dcl.type.splice] | |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Immediately following [dcl.type.simple] paragraph 3, add a new paragraph as follows:
Within a
([expr.const]).
Its value specifies the width
of the bit-precise integer type ([basic.fundamental]).
The program is ill-formed unless
([climits.syn])
or the denoted type is
.
of width 1
[Note:
can represent the values
and
,
but
is not a valid type.
— end note]
[cpp.predefined]
Add a feature-test macro to the table in [cpp.predefined] as follows:
7.2. Library
[cstdint.syn]
In [cstdint.syn], update the header synopsis as follows:
Change [cstdint.syn] paragraph 2 as follows:
All types that use the placeholder
,
,
, or
.
The exact-width types
and
for
,
,
, and
are also optional;
however, if an implementation defines integer types
other than bit-precise integer types
with the corresponding width and no padding bits,
it defines the corresponding
[Note:
The macros
and
correspond to the
and
,
respectively.
— end note]
[climits.syn]
In [climits.syn],
add a new line below the definition of
:
Change [climits.syn] paragraph 1 as follows:
The header ,
except that it does not define the macro .