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

C23 has introduced so-called "bit-precise integers" into the language, which should be brought to C++ for compatibility, among other reasons. Following an exploration of possible designs in [P3639R0] "The _BitInt Debate", this proposal introduces a new set of fundamental types to C++.

This draft is super early work-in-progress. The wording is not anywhere near complete, and the discussion needs to be fleshed out substantially.

Contents

1

Revision history

2

Introduction

3

Motivation

3.1

Computation beyond 64 bits

3.2

C ABI compatibility

3.3

Resolving issues with the current integer type system

4

Design discussion

4.1

Why not make it a library type?

4.1.1

Full C compatibility requires fundamental types

4.1.2

Tiny integers are useful in C++

4.1.3

Special deduction rules

4.1.4

Quality of implementation requires a fundamental type

4.2

Naming

4.2.1

Why no _t suffix?

4.2.2

Why the keyword spelling?

4.3

Should it be optional? Is this too hard to implement?

4.4

_BitInt(1)

4.5

Degree of library support

5

Implementation experience

6

Impact on the standard

7

Wording

7.1

Core

7.1.1

[lex.icon]

7.1.2

[basic.fundamental]

7.1.3

[conv.rank]

7.1.4

[conv.prom]

7.1.5

[dcl.type.simple]

7.1.6

[cpp.predefined]

7.2

Library

7.2.1

[cstdint.syn]

7.2.2

[climits.syn]

8

References

1. Revision history

This is the first revision.

2. Introduction

[N2763] introduced the _BitInt 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:

// 8-bit unsigned integer initialized with value 255. // The literal suffix wb is unnecessary in this case. unsigned _BitInt(8) x = 0xFFwb;

In short, the behavior of these bit-precise integers is as follows:

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:

_BitInt(32) plus( _BitInt(32) x, _BitInt(32) y); _BitInt(128) plus(_BitInt(128) x, _BitInt(128) y);

While one could rely on the ABI of uint32_t and _BitInt(32) 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 _BitInt supported by major compilers, and used by C developers:

Compiler BITINT_MAXWIDTH Targets Languages
clang 16+ 8'388'608 all C & C++
GCC 14+ 65'535 64-bit only C
MSVC

3.3. Resolving issues with the current integer type system

_BitInt as standardized in C solves multiple issues that the standard integers (int etc.) have.

Firstly, integer promotion can result in unexpected signedness changes.

The following code may have surprising effects if std::uint8_t is an alias for unsigned char.

std::uint8_t x = 0b1111'0000; std::uint8_t y = ~x >> 1; // y = 0b1000'01111

Surprisingly, y is not 0b111 because x is promoted to int in ~x, so the subsequent right-shift by 1 shifts one set bit into y from the left. Even more surprisingly, if we had used auto instead of std::uint8_t for y, y would be -121, 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 int.

Lastly, there is no portable way to use an integer with exactly 32 bits. std::int_least32_t and long may be wider, and std::int32_t is an optional type alias which only exists if such an integer type has no padding bits. While most users can use std::int32_t 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:

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 _BitInt is already supported by the compiler), shown below.

𝔽 – Fundamental type 𝕃 – Library type
template <size_t N> using bit_int = _BitInt(N); template <size_t N> using bit_uint = unsigned _BitInt(N); template <size_t N> class bit_int { private: _BitInt(N) _M_value; public: // ... }; template <size_t N> class bit_uint { /* ... */; };

The reasons why we should prefer the left side are described in the following subsections.

4.1.1. Full C compatibility requires fundamental types

_BitInt in C can be used as the type of a bit-field, among other places:

struct S { // 1. _BitInt as the underlying type of a bit-field _BitInt(32) x : 10; }; // 2. _BitInt in a switch statement _BitInt(32) x = 10; switch (x) {}

Since C++ does not support the use of class types in bit-fields, such a struct S 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 bit_int's may be useful as the underlying type of an enumeration:

enum struct Direction : bit_int<2> { north, east, south, west, };

By using bit_int<2> rather than unsigned char, every possible value has an enumerator. If we used e.g. unsigned char instead, there would be 252 other possible values that simply have no name, and this may be detrimental to compiler optimization of switch 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:

template <size_t N> void f(bit_int<N> x); f(int32_t(0)); // calls f<32>

Being able to make such a call to f 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 int, long, and _BitInt(32), as long as those three have the same width. The prospect of being able to write bit manipulation utilities that simply accept bit_uint<N> is quite appealing.

If bit_int<N> was a class type, this would not work because template argument deduction would fail, even if there existed an implicit conversion sequence from int32_t to bit_int<32>. 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 class bit_int 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, class bit_int is arguably adding pointless bloat.

For example, when an integer division has a constant divisor, like x / 10, 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 pre on e.g. bit_int::operator/ 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:

template <size_t N> using bit_int = _BitInt(N); template <size_t N> using bit_uint = unsigned _BitInt(N);

The goal is to have a spelling reminiscent of the C _BitInt spelling. There are no clear problems with it, so it is the obvious candidate.

4.2.1. Why no _t suffix?

While the _t suffix would be conventional for simple type aliases such as uint32_t, there is no clear precedent for alias templates. There are alias templates such as expected::rebind without any _t or _type suffix, but "type trait wrappers" such as conditional_t which have a _t suffix.

The _t suffix does not add any clear benefit, adds verbosity, and distances the name from the C spelling _BitInt. Brevity is important here because bit_int 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 _BitInt and unsigned _BitInt. While a similar approach could be taken as with the _Atomic 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:

Furthermore, to enable C compatibility, all of the spellings _BitInt, signed _BitInt and unsigned _BitInt need to be valid. This goes far beyond the capabilities that a compatibility macro like _Atomic can provide without language support. The most likely wording path would be to create an exposition-only bit-int spelling to define signed bit-int 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?!

Clang already supports the _BitInt 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, _BitInt(N) is only required to support N of at least LLONG_WIDTH, which has a minimum of 64. This makes _BitInt a semi-optional feature, and it is reasonable to mandate its existence, even in freestanding platforms.

Of course, this has the catch that _BitInt 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 _BitInt(1) but does permit unsigned _BitInt(1). This is an irregularity that could make generic programming harder in C++.

Whether _BitInt(1) should be permitted in C++ depends somewhat on the outcome of [N3644], a WG14 proposal which makes _BitInt(1) valid. That proposal also contains some practical motivation for why a single-bit should be permitted.

If _BitInt(1) was allowed, it would be able to represent the values 0 and -1.

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:

5. Implementation experience

_BitInt, formerly known as _ExtInt, 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 of
u U
long-suffix: one of
l L
long-long-suffix: one of
ll LL
size-suffix: one of
z Z
bit-precise-int-suffix: one of
wb WB

Change table [tab:lex.icon.type] as follows:

integer-suffix decimal-literal integer-literal other than decimal-literal
[…] […] […]
Both u or U and z or Z std::size_t std::size_t
wb or WB _BitInt(N) of minimal width N>1 so that the value of the literal can be represented by the type. _BitInt(N) of minimal width N>1 so that the value of the literal can be represented by the type.
Both u or U and
wb or WB
unsigned _BitInt(N) of minimal width N>0 so that the value of the literal can be represented by the type. unsigned _BitInt(N) of minimal width N>0 so that the value of the literal can be represented by the type.

Change [lex.icon] paragraph 4 as follows:

Except for integer-literals containing a size-suffix or bit-precise-int-suffix, if the value of an integer-literal cannot be represented by any type in its list and an extended integer type ([basic.fundamental]) can represent its value, it may have that extended integer type. […]

[Note: An integer-literal with a z or Z suffix is ill-formed if it cannot be represented by std::size_t. An integer-literal with a wb or WB suffix is ill-formed if it cannot be represented by any _BitInt(N) because the necessary width N is greater than BITINT_MAXWIDTH ([climits.syn]).end note]

[basic.fundamental]

Change [basic.fundamental] paragraph 1 as follows:

There are five standard signed integer types: signed char, short int, int, long int, and long long int. 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 _BitInt of width N for each 1<NBITINT_MAXWIDTH ([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 -2 N1 to 2 N1 1 (inclusive), where N is called the width of the type.

[Note: Plain ints 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: unsigned char, unsigned short, unsigned int, unsigned long int, and unsigned long long int. For each bit-precise signed integer type _BitInt of width N, there exists a corresponding bit-precise unsigned integer type unsigned _BitInt of width N. Additionally, there exists the type unsigned _BitInt of width 1. Likewise, 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 N as the corresponding signed integer type. The range of representable values for the unsigned type is 0 to 2 N1 (inclusive); arithmetic for the unsigned type is performed modulo 2N.

[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:

[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

can be converted to a prvalue of type int if int can represent all the values of the source type; otherwise, the source prvalue can be converted to a prvalue of type unsigned int.

Investigate whether in [conv.prom] paragraph 5, a converted bit-field of bit-precise integer type should be promotable. What does C do here?

[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 template simple-template-id
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
type-name the type named
simple-template-id the type as defined in [temp.names]
decltype-specifier the type as defined in [dcl.type.decltype]
pack-index-specifier the type as defined in [dcl.type.pack.index]
placeholder-type-specifier the type as defined in [dcl.spec.auto]
template-name the type as defined in [dcl.type.class.deduct]
splice-type-specifier the type as defined in [dcl.type.splice]
unsigned _BitInt(N) unsigned _BitInt of width N
signed _BitInt(N) _BitInt of width N
_BitInt(N) _BitInt of width N
charchar
unsigned charunsigned char
signed charsigned char
char8_tchar8_t
char16_tchar16_t
char32_tchar32_t
boolbool
unsignedunsigned int
unsigned intunsigned int
signedint
signed intint
intint
unsigned short intunsigned short int
unsigned shortunsigned short int
unsigned long intunsigned long int
unsigned longunsigned long int
unsigned long long intunsigned long long int
unsigned long longunsigned long long int
signed long intlong int
signed longlong int
signed long long intlong long int
signed long longlong long int
long long intlong long int
long longlong long int
long intlong int
longlong int
signed short intshort int
signed shortshort int
short intshort int
shortshort int
wchar_twchar_t
floatfloat
doubledouble
long doublelong double
voidvoid

Immediately following [dcl.type.simple] paragraph 3, add a new paragraph as follows:

Within a bit-precise-int-type-specifier, the constant-expression shall be a converted constant expression of type std::size_t ([expr.const]). Its value N specifies the width of the bit-precise integer type ([basic.fundamental]). The program is ill-formed unless 1 < N BITINT_MAXWIDTH ([climits.syn]) or the denoted type is unsigned _BitInt of width 1.

[Note: unsigned _BitInt(1) can represent the values 0 and 1, but _BitInt(1) is not a valid type. — end note]

[cpp.predefined]

Add a feature-test macro to the table in [cpp.predefined] as follows:

__cpp_bit_int 20XXXXL

7.2. Library

[cstdint.syn]

In [cstdint.syn], update the header synopsis as follows:

namespace std { […] using uintmax_t = unsigned integer type; using uintptr_t = unsigned integer type; // optional template<size_t N> using bit_int = _BitInt(N); template<size_t N> using bit_uint = unsigned _BitInt(N); }

Change [cstdint.syn] paragraph 2 as follows:

All types that use the placeholder N are optional when N is not 8, 16, 32, or 64. The exact-width types intN_t and uintN_t for N = 8, 16, 32, and 64 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 typedef-names. Each of the macros listed in this subclause is defined if and only if the implementation defines the corresponding typedef-name.
[Note: The macros INTN_C and UINTN_C correspond to the typedef-names int_leastN_t and uint_leastN_t, respectively. — end note]

[climits.syn]

In [climits.syn], add a new line below the definition of ULLONG_WIDTH:

#define BITINT_MAXWIDTH see below

Change [climits.syn] paragraph 1 as follows:

The header <climits> defines all macros the same as the C standard library header limits.h, except that it does not define the macro BITINT_MAXWIDTH.

8. References

[N5014] Thomas Köppe. Working Draft, Programming Languages — C++ 2025-08-05 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/n5014.pdf
[N2763] Aaron Ballman, Melanie Blower, Tommy Hoffner, Erich Keane. Adding a Fundamental Type for N-bit integers 2021-06-21 https://open-std.org/JTC1/SC22/WG14/www/docs/n2763.pdf
[N2775] Aaron Ballman, Melanie Blower. Literal suffixes for bit-precise integers 2021-07-13 https://open-std.org/JTC1/SC22/WG14/www/docs/n2775.pdf
[N3644] Robert C. Seacord. Integer Sets, v2 2025-07-05 https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3644.pdf