std :: big_int
- Document number:
- P4444R0
- Date:
2026-09-22 - Audience:
- SG6
- Project:
- ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21
- Reply-to:
- Jan Schultke <janschultke@gmail.com>
Matt Borland <matt@mattborland.com>
Christopher Kormanyos <ckormanyos@yahoo.com> - GitHub Issue:
- wg21.link/P4444/github
- Source:
- github.com/eisenwave/cpp-proposals/blob/main/src/big-int.cow
Contents
Introduction
Infinite-precision integers in other languages
Motivation
Use cases
std :: big_int is a vocabulary type
std :: big_int for convenience and correctness
std :: big_int is platform-dependent and hard to implement
std::big_int should be a compiler intrinsic for constant evaluation
Design
Design strategy
Layout
std::uint_multiprecision_t
C compatibility
Small object optimizations
SOO customization
Layout in our reference implementation
Sign and magnitude
Bitwise operations
Representation endianness
Access to the underlying representation
Why not provide access using a random access range?
Why not use reference counting?
Why not use tagged integers?
Problems with extendability
std::big_int is not a container
constexpr support
Circumventing transient allocations
Optimal operator overloads
Why not return by && from unary operators?
Why not provide lower-level operations for arithmetic?
Why no std::big_int_view ?
Issues with std::big_int_view parameters
Issues with std::big_int_view allocation management
std::big_int_view is just not useful enough
Why no std::big_uint ?
Burning the std::big_uint bridge
Conclusion
Template parameters
Order of template parameters
min_inplace_capacity
min_inplace_capacity restrictions
Limb type parameter
Conversions
Converting constructor
Explicit conversions to arithmetic types
Bit-precise integer interoperability
Expression templates (or lack thereof)
Technical reasons against expression templates
The problem with optional expression templates
Expression templates in the ISO C++ process
User-defined literals for std::big_int
Why 123n ?
User-defined literals vs. string constructors
Why provide UDLs or string constructors at all?
Adjacent library utilities
Hash support for std::big_int
std::numeric_limits specialization
std::in_range
std::abs
std::gcd , std::lcm , and std::midpoint
std::saturating_cast
std::to_chars and std::from_chars
Formatting support
std::to_string and std::from_string
No <bit> support
Naming
Choice of header
Error handling
Future direction
Integer numeric functions
Fixed-point interoperability
Random number generation
Making std :: uint_multiprecision_t C-compatible
Non-transient allocations
Constant template parameters
Arguments against standardization
Many trade-offs
Use of committee resources
Increased complexity of future integer features
Use of implementer resources
Residual fragmentation
N-to-N conversion problem
Inability to change ABI
Header bloat
Implementation experience
Deployment experience
Benchmarks
Wording
[headers]
[version.syn]
[numeric.limits]
[numeric.limits.general]
[numeric.special]
[utility]
[utility.syn]
[utility.intcmp]
[numeric.ops.overview]
[numeric.ops.gcd]
[numeric.ops.lcm]
[numeric.ops.midpoint]
[numeric.sat.cast]
[numeric.int.div]
[string.syn]
[string.conversions]
[big.int]
[big.int.general]
[big.int.syn]
[big.int.class]
[big.int.require]
[big.int.defns]
[big.int.expos]
[big.int.cons]
[big.int.ops]
[big.int.modifiers]
[big.int.conv]
[big.int.unary]
[big.int.alias]
[big.int.cmp]
[big.int.binary]
[big.int.hash]
[big.int.fmt]
[big.int.literal]
[charconv]
[charconv.syn]
[charconv.to.chars]
[charconv.from.chars]
[c.math.abs]
Acknowledgements
References
1. Introduction
C++ currently provides no portable support for integers wider than 64 bits.
This is one of the oldest and most widely recognized gaps in the language,
which authors have attempted to fill several times:
[N1692] (2004), [N1744] (2005), [N2143] (2007), and [N4038] (2014)
all proposed a class with infinite precision.
Each of these attempts failed —
not because there was a lack of interest or motivation,
but because the task turned out to be too great of a challenge.
is to
what is to :
sure, a fixed size is sufficient for some use cases,
but it comes with a constant threat of overflow and undefined behavior,
and that threat is easily eliminated at some acceptable runtime cost.
1.1. Infinite-precision integers in other languages
The runtime cost of infinite-precision integers is often acceptable
to the point where the default integer
in many languages is not fixed-size.
Even when infinite-precision is not the default,
languages often provide an infinite-precision type in their standard library,
or there is a commonly used third-party library that the ecosystem uses:
| Language | Integer type / constraint | Kind |
|---|---|---|
| Python | builtin | |
| Ruby | builtin | |
| Lisp | builtin | |
| Scheme | builtin | |
| Racket | builtin | |
| Clojure | builtin | |
| Haskell | builtin | |
| Prolog | builtin | |
| Wolfram Language | builtin | |
| Maxima | builtin | |
| Erlang | builtin | |
| JavaScript / TypeScript | builtin, but not the default | |
| Java / Kotlin | standard library | |
| Julia | standard library | |
| C# | standard library | |
| F# | standard library | |
| Visual Basic .NET | standard library | |
| Go | standard library | |
| Perl | standard library | |
| Ada | standard library | |
| Zig | standard library | |
| Standard ML | standard library | |
| MATLAB | Symbolic Math Toolbox | standard library |
| PHP | GMP and BCMath | standard library |
| R | third-party package | |
| Swift | third-party package | |
| Rust |
|
third-party package |
Beyond that,
there are also many programming languages where some third-party support is available
(e.g.
Furthermore,
even when the language doesn't provide an infinite-precision integer type,
this is typically part of the compiler regardless.
For example, MSVC, GCC, and LLVM all internally have an infinite-precision integer,
such as .
Any C23 compiler also needs such a type to implement optimizations
like constant folding for ,
unless it has a small .
Not only are infinite-precision integers available in many languages, they are also used frequently:
| Language | GitHub code search | # Files |
|---|---|---|
| TypeScript |
|
1.4M |
| Java |
|
1.3M |
| JavaScript |
|
811K |
| C++ |
|
404K |
| Rust |
|
104K |
The tables also do not include languages where no active choice to use infinite-precision integers
over fixed-width integers is made.
Otherwise, the results would be inflated by e.g. 107M files using in Python.
2. Motivation
2.1. Use cases
In terms of utility,
use cases can be classified into two buckets:
- The use case where integers are potentially big. That is, the numbers involved are typically still small, but since no fixed-width integers are used, they might sometimes be big.
- The use case where integers are actually big.
has many use cases,
falling into one of those buckets:
| Use case | Goal | Bucket |
|---|---|---|
| Serialization | JSON, YAML, etc. support integers without any upper bound set by the markup format, and large integers need to be represented somehow. | Potentially big |
| Safety | Avoiding UB or correctness issues from integer overflow, at some runtime cost. | |
| Scripting engines |
Scripting languages often support infinite-precision integers
(e.g. Python's ),
and their engines need to represent those.
|
|
| Statistics/combinatorics | Large factorials require much wider integers than 64-bit. | Actually big |
| Scientific computing | Binomial coefficients, primality tests, integer relation algorithms, partition numbers, and many more may be computed with thousands of bits. | |
| Cryptography | Many cryptographic algorithms such as RSA require calculations with hundreds or thousands of bits. |
2.2. std :: big_int is a vocabulary type
An important reason why should be in the C++ standard library
is that it is a vocabulary type and may appear at library boundaries.
:
-
A library that loads config files (JSON, YAML, etc.)
can return deserialized integers as
.std :: big_int -
Those
objects are then processed by a numeric library that uses them in e.g. statistics.std :: big_int -
The results are then stored by another library that serializes
to e.g. CSV. Alternatively, the results are stored in a relational database, using a C++ API that usesstd :: big_int for big integer columns in a table.std :: big_int
,
such as and .
These higher-level vocabulary types are then used in various other parts of the
networking and cryptography stack,
like the TLS implementation, Java Key Stores, etc.
This is also a plausible use case in C++.
While RSA calculations may internally use fixed-width integers,
simplifies top-level interfaces by type-erasing
the width.
That is, the integer type is the same,
regardless of the RSA key size or other constants.
,
an infinite-precision decimal floating-point type can be formed as follows:
The value represented here is .
Such a type would appear at a library boundary for communicating with a PostgreSQL database,
which supports and among its
Numeric Types.
Note that PostgreSQL supports potentially huge decimal types,
like up to 131072 before the decimal point
,
so even with C23 types like ,
those could not be represented.
These types map onto the type in Java libraries.
big integer
or big decimal
.
The workaround for C's and C++'s lack of infinite-precision types in libpq
(the PostgreSQL C library)
is to either
- represent integers and decimal numbers as strings, or
-
convert to libpq's binary numeric format
(and the conversion from e.g.
to this format is fairly complicated).boost :: multiprecision :: cpp_int
In Java, such interfaces are obvious and ergonomic by comparison,
operating on or .
This also makes it possible to build higher-level database abstractions
like the JDBC API
on top of concrete database drivers
(for Microsoft SQL, PostgreSQL, MySQL, H2, etc.)
without converting big numeric types at each layer.
By comparison, if someone wanted to create a high-level, generic API for databases in C++,
they would not have a common vocabulary type for big integers that can be passed through
the various API layers unchanged.
is that crucial missing type.
big integer
class,
possibly as a wrapper around gmp.
For example, there is
SymEngine::Integer,
GiNaC::numeric, etc.
This is also typically the case for scripting language engines if the language
has a big integer type (like Python or JavaScript).
This ultimately means that a single application can end up with several big integer
implementations at the same time to implement those wrappers:
some manual
,
some using a statically linked
can solve that problem by turning all these fragmented wrappers
into wrappers around a single standard type,
or by removing the wrappers altogether.
Libraries can simply use directly
just like they use directly.
inputs and
Qt's .
Such limitations are arbitrary and unnecessary when infinite-precision numeric types
are available for use in GUIs.
While the internal editing state of a widget needs to be represented as text
(to represent partial inputs like
before the user types the fractional part),
input and output of a or value is clearly useful
for number fields.
In some sense,
is an even more important vocabulary type
than and
because there is no convenient fallback solution.
If those containers were not available,
users could still communicate at library boundaries by using pointers and sizes.
Strings can be passed to other libraries as a (and maybe ),
so while is an important container type
and helps with managing allocation ownership,
it is arguably gratuitous as a vocabulary type.
Similarly, if and were not available,
they could easily be replaced with the pointer and the size that they wrap.
By comparison to contiguous containers and views,
there is no widely established convention for passing large integers between libraries in C++,
other than using their string representation,
which comes at significant cost.
Consequently, we see a fragmented ecosystem in C++,
where every logging, database, cryptography, and serialization library
has its own solution to passing big integers and often its own types.
could lay the foundation for passing big integers between libraries
just as easily as in other languages.
is not directly C-compatible,
one could pair a and ,
where the passed limb array
has the same layout as .
This means that such a C++ type might establish a valuable convention
for passing big integers in C libraries,
even if not directly available.
2.3. std :: big_int for convenience and correctness
Another reason why needs to be in the standard library
is that users would often avoid it otherwise,
even when it is the right tool for the job.
For example, it could be quite plausible that a library has to handle integers
with more than 64 bits in some edge cases,
but if the library author needs to add a dependency on the whole of Boost.Multiprecision
to replace 2-3 lines of code using
with ,
they may just not do it.
C++ developers don't get a fair choice between a fixed-width type for performance and an infinite-precision type for correctness; they get to choose between readily available fixed-width types and adding a heavy dependency like GMP or Boost.Multiprecision for correctness, which they may not do, especially if that dependency is transitively forced onto their library users.
2.4. std :: big_int is platform-dependent and hard to implement
Another reason why needs to be in the standard library
is that it's extremely difficult to implement and optimize,
in part because the implementation depends heavily on the platform's hardware capabilities,
many of which are not exposed portably in the language.
-
x86_64 has a
mul instruction that yields both the low and the high bits of a multiplication; without utilizing that, multiprecision multiplication is much slower. -
x86_64 also has a
div instruction that takes a 128-bit dividend and a 64-bit divisor, which can be used to implement division of an arbitrary-length dividend much more efficiently. -
Funnel-shifting (basically a 128-bit shift)
is a common operation in multiprecision arithmetic,
and Clang recently got a
intrinsics that exposes the hardware instruction.__builtin_elementwise_funnel_shl -
Bitwise operations between
can be performed in parallel using SIMD intrinsics. While those can be portably implemented usingstd :: big_int now, that may add unnecessary overhead compared to using the underlying intrinsics directly.<simd> -
Conversions between
and floating-point types may be hardware-accelerated or backed by special routines in the runtime library. For example, to convertstd :: big_int tofloat , the conversion should go throughstd :: big_int (__fixunssfti →float ) followed by a conversion tounsigned __int128 . Note that an unsigned 128-bit integer is sufficient to represent any finite binary32 floating-point number without the fractional part.std :: big_int -
During constant evaluation,
if
is available, it is often faster to implement an operation in terms of_BitInt . For example, a 4096-bit division can be delegated entirely to compiler intrinsics by converting the dividend and divisor to_BitInt , performing the division in terms of e.g._BitInt ( 4096 ) operations. This completely avoids the need for constant evaluation of complex division routines.llvm :: APInt
In general, the intrinsics necessary to implement efficiently
vary wildly from compiler to compiler
and from architecture to architecture.
It also varies a lot whether those intrinsics can be used during constant evaluation,
and these implementation details are subject to frequent change and added features.
Our [Reference-Impl] of is a sea of
and checks.
Overall, it is practically impossible or at least extremely difficult for a third-party library to keep up with all these details to implement the type optimally, across all compilers and all supported architectures. Features like these should ideally live directly in the compiler or in the standard library.
2.5. std::big_int should be a compiler intrinsic for constant evaluation
While a portable implementation of is possible,
an ideal implementation for constant evaluation simply delegates
to the compiler's internal infinite-precision integer type (e.g. in LLVM).
This is because constant evaluation is relatively expensive
compared to highly optimized multiprecision code in the compiler internals.
A third-party library such as Boost.Multiprecision
does not have the luxury of adding new compiler intrinsics,
so the implementation quality could never match the potential of .
A closely related issue is the implementation of the user-defined literal, which has the following interface in our proposal:
The problem is that there is no other form of
that enables this syntax,
and the form requires manual parsing of the digits during constant evaluation,
rather than delegating this to the compiler.
It would be much better if parsing could be done in the compiler
(and it already does actually)
to provide a form such as:
With this form,
- digit separators would be skipped automatically,
- base prefixes like
would be handled by the compiler, and0x - forming the limb array would be done by the compiler, which is the expensive part,
- template instantiations would be avoided completely.
That being said,
this new form of is not part of this proposal,
but it does demonstrate why some compiler support is needed.
3. Design
3.1. Design strategy
The most important anchor points of the design are:
-
should be elastic; that is, it should grow to the necessary size required to fit the result of an arithmetic operation.std :: big_int -
should support custom allocators.std :: big_int -
should exposestd :: big_int small object optimization
to the user. -
should grant access to the underlying representation.std :: big_int -
operations should bestd :: big_int .constexpr
This culminates in the following library declarations:
Conceptually, a holds:
The underlying arithmetic is then implemented to operate on limb arrays,
i.e. arrays of objects,
agnostic of whether the integer value is stored in-place or dynamically.
3.2. Layout
3.2.1. std::uint_multiprecision_t
A user-facing integer type alias introduced by this paper is ,
which is an unsigned integer type with the following important properties:
-
has no padding bits. Padding bits in a limb type would be wasteful and make things more difficult both for users and for implementations.std :: uint_multiprecision_t -
is the widest type that has arithmetic hardware support.std :: uint_multiprecision_t -
has no explicit minimum width, but effectively cannot be narrower thanstd :: uint_multiprecision_t due to being unpadded.unsigned char
In summary, it is the unsigned integer type most suitable for multiprecision. Colloquially it is the largest unsigned integer which has arithmetic instructions.
Curiously, there is no existing type with the desired properties
despite the abundance of type aliases in and :
-
is at least 32 bits wide, which means that on 16-bit architectures, the multiprecision code would have two layers: the emulation of 32-bit integers on 16-bit hardware, and then the multiprecision ofstd :: uint_fast32_t operations. This is highly questionable because the point ofstd :: big_int is to have a type whose operations correspond directly to hardware instructions. Also,std :: uint_multiprecision_t is not consistently 64 bits wide on targets with 64-bit arithmetic support.std :: uint_fast32_t -
often could be used asstd :: size_t , but on targets like WASM32, it is only 32 bits wide despite WASM32 having 64-bit arithmetic instructions forstd :: uint_multiprecision_t i64 . It would be extremely wasteful to only use 32-bit operations when 64 bits are available. -
typically has the same problem asstd :: uintptr_t .std :: size_t -
is only 32 bits wide on 64-bit Windows.unsigned long -
is always 64 bits wide, even on 32-bit targets.unsigned long long
3.2.2. C compatibility
While is not really C-compatible by virtue of being a class template
full of C++ features,
it is still possible to provide a C-compatible interface
taking and implementing its functionality in C++.
This relies on the fact that the internal layout of is specified in detail.
This is analogous to having a C header taking
and implementing the function internally using in C++.
However, this somewhat assumes that
also becomes available in C,
possibly in / .
Not having the type in C means that C users would need to recreate the type choice
the implementation makes or use implementation-specific definitions like a
predefined macro.
C compatibility ambitions are not in scope for the paper right now, but are worth exploring in the future.
(possibly coming to C++29 via [P3666R4]).
3.2.3. Small object optimizations
(or generally, infinite-precision integer types)
benefit from small object optimization
perhaps more than any other vocabulary type.
This is because with a relatively small amount of in-place storage
(e.g. 8 bytes for 64-bit numbers),
a lot of applications would never need to allocate dynamic storage to hold the value,
or only for extreme and unusual program inputs.
Furthermore,
once the numbers are huge enough to require dynamic allocation,
the cost of multiprecision arithmetic often hugely outweighs
the small amount of overhead caused by having a few unused bytes of in-place storage.
,
but many strings exceed the in-place capacity (around 20 bytes),
so dynamic allocation for fairly short strings remains common.
The optimization is also necessary to prevent excessive allocations for small intermediate results.
For example, even for huge inputs and ,
the difference , the quotient ,
or the remainder could still be fairly small numbers (possibly zero),
and it is wasteful to dynamically allocate for tiny integers.
using SOO performs operations like addition
4×–10× faster than without SOO.
See https://eisenwave.github.io/std-big-int/benchmarks.html#benchmarks_small_value.
3.2.3.1. SOO customization
Because performing this optimization for is such a no-brainer
and any reasonable implementation should have it,
we propose to expose it to the user via the template parameter.
This parameter specifies the minimum integer width that
must be able to represent without dynamic allocation.
While should be used in most cases and provides a reasonable default,
there may be scenarios where
-
on 32-bit platforms, the capacity could be lowered to
or raised to32 to prefer performance,64 -
on platforms where allocations are particularly expensive
or where dynamic storage is very limited,
the capacity could be raised to
or even128 .256
template parameter
of .
See also Boost documentation for
3.2.3.2. Layout in our reference implementation
is always 64 regardless of architecture.
On 64-bit, our layout is as follows:
This layout ensures that
- is two pointers large,
- is aligned to one pointer,
- is nothrow-movable and trivially relocatable,
- can represent both
andint64_t values without allocating, anduint64_t - can represent values with up to bits (137 billion bits).
For reference,
OpenJDK's
3.2.4. Sign and magnitude
uses a sign-and-magnitude representation
(as opposed to e.g. two's complement),
and this is exposed to the user.
While it may seem like constraining implementation freedom unnecessarily,
the chosen representation impacts the complexity requirements of various operations.
For example, if the sign bit is separately stored,
then negation can be implemented in constant time by just flipping the sign bit,
whereas for two's complement,
it requires linear time because bits of the magnitude need updating.
Most implementations of infinite-precision integers use sign-and-magnitude representation, as does our [Reference-Impl]. This is not only due to those beneficial complexity requirements, but also because it simplifies the implementation greatly. For example: multiplicative operators like multiplication and division can be implemented solely in terms of the magnitude, and the sign bit of the result can be computed separately.
3.2.4.1. Bitwise operations
The only major downside of sign-and-magnitude representation is that it requires emulation of two's complement for bitwise operations, but this is relatively cheap and does not require an additional pass over the data.
with negative inputs
in such a way that is consistent with the two's complement behavior.
To do this in a single pass, for each limb, we need to
- negate each of the operand limbs if they belong to a negative
,std :: big_int - perform a bitwise AND, and
- negate the resulting limb if the sign bit of the result is negative.
is equivalent to ,
so these on the fly negations
are done by performing a bitwise NOT and interleaving
the whole operation with an add-with-carry.
In bitwise operations,
negative inputs are treated as if they were preceded by an infinite sequence of leading ones, and
positive inputs are treated as if they were preceded by an infinite sequence of leading zeros.
The sign bit of the result can be computed before any of the result limbs
by performing the bitwise operation on the leading infinite bit value
of the operands.
3.2.5. Representation endianness
There are two options for the endianness
of the representation:
- Most significant limb first (big endian).
- Least significant limb first (little endian).
For some operations, there is no significant difference in performance between these two. However, these are far from equally good options in specific cases. In particular, there are many common operations that shrink or truncate, like
,&= 0x FF ,%= 10 - subtraction between two close values,
- etc.
Whenever these operations are performed in-place, and we are left with, say, a single limb, that limb is already in the right place; only the limb count stored in the container needs to be updated.
The key insight is that truncation of little-endian representations is much cheaper because no limbs need to be moved. Similarly, zero-extension or sign-extension moves no limbs in a little-endian representation, but requires moving all limbs in a big-endian representation.
operation,
for little-endian representation,
we just need to perform a bitwise between the least significant limb and .
The limb count is always set to after this operation.
For big-endian representation, we also need to move the least significant limb to the front of the representation.
In practice, every implementation known to us uses little-endian representation because it is strictly better for the operations that are performed on integers in practice.
3.2.6. Access to the underlying representation
As mentioned in §3.1. Design strategy,
a is conceptually a of
and
.
This storage should be directly accessible using a function.
Currently, we facilitate this using:
The obvious downside of giving the user such direct access is that it breaks encapsulation,
and it constrains the small object type
that the implementation could use.
For example, it isn't possible to have an
because cannot alias .
Despite those downsides,
giving the user low-level access to the storage is necessary because it enables the user
to provide additional operations on without overhead.
function
(counting the number of one-bits within the integer):
Without this kind of low-level access,
the user would have to repeatedly perform bitwise operations such as and
in order to perform the operation limb-by-limb.
Furthermore, it is unclear how the operation could be performed without allocations.
Even if the result of fits into a 64-bit integer and needs no allocation,
any intermediate result of may require allocation.
member function.
Crucially, we only provide read access to the representation, not write access.
Providing write access would make it easily possible to put the
into a corrupt state, like
having a that doesn't match the number of bits in the representation, or
producing negative zero.
3.2.6.1. Why not provide access using a random access range?
Rather than providing a to the underlying representation,
it would also be possible to provide a random access range.
This would grant some more flexibility in the representation, like:
-
Storing an
or other type rather than aunsigned __int128 for in-place storage.uint_multiprecision_t [ ] -
Using
tagged integer
representations (see §3.2.8. Why not use tagged integers?).
not exposing implementation details
has also been used against providing access to the data of
in https://github.com/rust-num/num-bigint/issues/283.
That Rust crate instead provides a random-access iterator via
BigUint::iter_u64_digits.
However, that implementation stores a (or array thereof)
in the small and large case,
so there is no concrete benefit to the encapsulation.
However, we decided against random access iterators for a number of reasons:
-
If the implementation actually makes use of the added design freedom and computes
the
representation on the fly in the small case, the iterator needs to check whether the integer is small or large on every iteration. This is especially annoying when theuint_multiprecision_t currently holds an allocation, which is definitely just an array ofstd :: big_int in memory. We are concerned about the performance overhead.uint_multiprecision_t -
As explained above, the benefit of providing access to the limb array is that the user
can extend the type with additional operations without overhead.
If
returns arepresentation ( ) , the user can actually do that. Otherwise, there should actually be astd :: span returning adynamic_representation ( ) for the large case. Such a function would inevitably be used by the standard library to implement its operations without overhead. Even if provided, users would now need to handle the small and large cases separately.std :: span -
Getting access to the underlying contiguous range allows for less overhead
and more SIMD-friendliness.
If the user wanted to compare to their ownstd :: big_int (an operation we don't directly provide but might be added in the future via astd :: span < const uint_multiprecision_t > type), this could be done by directly loading (unaligned) from thestd :: big_int_view bytes and from thestd :: span representation bytes into SIMD registers, then comparing 16-64 bytes in one SIMD instruction. In fact, our implementation delegatesstd :: big_int tooperator <=> .std :: memcmp By contrast, if the user only had access to a random access iterator, they would need to repeatedly assemble N results of
; they could not simply calloperator * .std :: memcmp -
As explained in §3.2.2. C compatibility,
providing a
enables C interoperability. The application may internally hold astd :: span and pass its representations to C APIs asstd :: big_int anduint_multiprecision_t [ ] , similar to howsize_t can interface with C thanks tostd :: string .c_str ( )
It is also important to understand that the C-compatible interface has implications
even beyond .
multiprecision operation
(from the perspective of a C++ implementation),
all sorts of code paths can converge on a single runtime library function:
This would ultimately mean that on ,
, and
can all utilize the same routine without overhead.
3.2.7. Why not use reference counting?
An idea very early in the design process was to use reference counting in order to avoid
the issue explained in §3.4. Optimal operator overloads.
That is, if was essentially a
,
it would be possible to pass it cheaply by value.
If has unique ownership (i.e. the reference count is 1),
can modify the integer in-place;
otherwise, performs a copy.
While this idea seems clever at first, there are several problems:
-
The same was historically attempted for types like
to no success. Boost implementers gave negative feedback on this idea being pursued forstd :: string , and Boost.Multiprecision performs no reference counting either. A key problem is difficulty of implementation.std :: big_int - Reference counting needs to be atomic because in the standard library, read-only access like copy constructors is generally thread-safe. Atomic operations can have significant overhead.
-
Actually making use of the optimization opportunity in the example above
(that is, modifying
's data in-place if it has unique ownership) requires an atomic compare exchange (CAS): if the reference counter is currently one, set it to zero, otherwise other threads might believe they have unique ownership too. That also makes thestd :: big_int in the example necessary; we need permission to mess with the original object. Even with no contention, a CAS is a relatively expensive instruction.std :: move
By comparison, a design with unique ownership
avoids a lot of problems because it is statically known that it has unique ownership;
no runtime check is needed.
3.2.8. Why not use tagged integers?
A potential alternative layout can be provided using tagged integer
techniques,
as explained in [MSR-TR-2022-17].
small
integer
Addition would look something like:
As stated in the paper:
This encoding is widely used, ranging from statically typed languages like OCaml to dynamic languages like Ruby, Common Lisp, and some JavaScript implementations.
The paper explores further improvements upon the technique where two least significant bits
are used in the short representation,
which makes the happy path
even faster because it eliminates the
statement at the start,
among other improvements.
Overall, such techniques provide two key benefits:
- The container size is aggressively reduced to just a single pointer. By comparison, the layout in our reference implementation is two pointers large on 64-bit.
-
The
happy path
for small integers is faster because it avoids a branch. At the assembly level, in the improved approach described in the paper, addition compiles down to just anadd instruction followed by two cheap checks and easily predictable branches.
That being said, we decided not to pursue this approach for a number of reasons:
-
If one actually wants to reduce the size to just a single pointer,
not every 64-bit integer can be represented anymore.
Many use cases involve just that, such as
- reinterpreting
as a 64-bit integer,double - generating random numbers 64 bits at a time,
- performing modular arithmetic with large 64-bit moduli,
- using a 64-bit bit mask or bit pattern for bit manipulation,
- computing 64-bit hashes,
- …
- reinterpreting
-
As explained in §3.2.6. Access to the underlying representation,
it is crucial for us to provide direct access to the representation of the integer.
The tagged pointer approach implies we cannot simply return
a
but would need to return some kind ofspan < const uint_multiprecision_t > between anunion (for small values) and aarray (for large values). This is substantially more complicated and makes thespan happy path
slower as well. -
The tagged integer approach relies heavily on assumptions about pointer representation
and on the ability to reinterpret pointers as integers and vice versa.
The
support is hard to implement withoutconstexpr compiler magic
because we would do
, which is undefined behavior in C++. The only way around that is to always hold an allocation in thetype punningunion case, which sacrifices the ability to hold anyconstexpr values in astd :: big_int variable (except zero perhaps), increases the complexity of the implementation, and presumably makes constant evaluation slower.constexpr
3.2.8.1. Problems with extendability
Furthermore,
it is impossible for the user to extend the set of operations without overhead
because the user has no access to the lowest-level details
needed.
Consider that in the example above, we can implement optimally,
ending in
because we know exactly how the tagged integer is represented.
Similarly, multiplication and division need to keep in mind that we are essentially performing a fixed-point operation with a constant fractional digit and require some bit-shifting and adjusting to handle that. Every operation deals with the issue slightly differently.
function ideally
(which is a motivating example in §3.2.6. Access to the underlying representation):
This example relies entirely on super-low-level, secret knowledge
of representation details.
A standard library implementer has access to that,
but users would only have the public API to work with.
By comparison, the proposed layout involves a
directly exposed to the user,,
so the API does very little hiding
and abstracting
.
The user could actually implement a function that is not a single CPU cycle
slower than a standard library implementer
when only using the public API.
3.2.9. std::big_int is not a container
When looking at the API of ,
one may be tempted to think of it as a contiguous container and even to define it as such.
That is, rather than having ,
could have and iterators, etc.
There are a few problems with this approach:
-
It would be surprising for
to simultaneously act like an integer and a container.std :: big_int -
Code that calls
and other member functions would always be interacting withsize ( ) at the low-level representation level, which is not what most users should do. In our design,std :: big_int yields the size in bits, which abstracts from the width ofstd :: big_int :: size ( ) .std :: uint_multiprecision_t -
In our design, there is always at least one limb.
This is in part because with small object optimization,
there is always at least one limb of in-place storage anyway.
This is convenient for anyone relying on
always being valid, but makesrepresentation ( ) . front ( ) andclear ( ) weird since it's not actually possible to downsize the container to zero limbs.resize ( ) -
In our design, code that iterates over the representation does so using
, which makes it abundantly clear that we are not iterating over, say, each bit. This clarity would be lost if we iterated directly overfor ( auto limb : x . representation ( ) ) .x -
If
were a sequence container, it should presumably havestd :: big_int ,push_back , etc. However, it's inherently confusing whatappend back
refers to in this context: in English, numbers are written from most significant to least significant, so theback
is the least significant digit, but in ourrepresentation, thestd :: big_int back
is the most significant limb. By contrast, it is clear thatis the most significant limb if the user knows how the representation is structured.representation ( ) . back ( ) -
Because the sign in the representation is separate from the limbs,
it doesn't neatly fit into the mental model of a container.
If there was a
function that assigns a range of limbs, it couldn't assign the sign bit, so it would be a partial assignment and should be calledassign orassign_magnitude . However, if we need to clarify what we're assigning, this indicates that the idea ofassign_representation is simply a container is unnatural. We would also need to check whether the call tostd :: big_int generated a negative zero.assign -
There is a difference in terminology between
and a container. For example, reducing the size of an integer is calledstd :: big_int truncating
, but the container function is called. Similarly, setting to zero should be spelledresize ( ) orset_zero ( ) , not= 0 .clear ( )
That being said,
still has an API that is similar to a contiguous container
for the purpose of familiarity.
This includes functions such as ,
,
(in bits),
(amount of bits),
etc.
These are mostly about allocation management,
not about iterating over the representation.
3.3. constexpr support
also provides operations,
similar to .
Without any dedicated compiler intrinsics,
this means that manages an allocation,
similar to .
That allocation is transient, i.e. it cannot persist to runtime.
However, performing operations on a and freeing all memory
before the end of the constant expression is fine.
There is another layer to the design here,
which is that is mandated to perform small object optimization,
and this is exposed to the user in the public API,
rather than just being an optional optimization like for .
Because of this, we can have values persist to runtime
as long as the value is small enough to require no allocation.
3.3.1. Circumventing transient allocations
One additional restriction on implementations to make this behavior reliable
is that they must never represent a dynamically
when inplace storage is sufficient.
At runtime, implementations might hold onto an allocation until
is called because that allocation may be immediately useful in the next operation.
Even if subtracting would make the difference small enough to be represented
as a 64-bit integer,
we do not (and should not) require freeing the storage of at runtime.
That allocation is immediately useful to represent the result of the bit-shift.
However, at compile time,
we require that every operation behaves as if
was called on the result,
so allocations are aggressively freed if at all possible.
This is necessary because the result of the subtraction in the example
might be stored in a variable,
and this would fail unnecessarily because of allocation restrictions.
If we didn't have that behavior,
variables might randomly
fail to compile
from the perspective of the user, depending on the compiler,
for no good reason.
3.4. Optimal operator overloads
The design includes operator overloads that are optimal
,
which requires handling two cases:
-
When an operand is an rvalue of type
, its allocation can be reused for the operation.basic_big_int -
When an operand is of integral type,
no temporary
is created. This is important because the type may bebasic_big_int ,__int128 , etc. and may not be representable as_BitInt ( 1024 ) without an unnecessary allocation.basic_big_int
Unary operators become overload sets, like:
Each such binary operator must handle eight distinct cases, which the [Reference-Impl] classifies as follows:
// Convenience macro that describes one of eight forms of binary operation // that any binary operation for `big_int` can take. enum struct binary_op_form : unsigned char { // Both sides are movable. // Typically, the operation is performed by mutating the integer // with the most capacity. move_move , // Only the left side is movable, and its allocation is reused. // The right side is also a `basic_big_int`, but not movable. move_copy , // Only the right side is movable, and its allocation is reused. // The left side is also a `basic_big_int`, but not movable. copy_move , // Neither side is movable, so a fresh `basic_big_int` is created. copy_copy , // The left side is movable, and its allocation is reused. // The right side is a fundamental integer (of any cvref qualification). move_int , // The right side is movable, and its allocation is reused. // The left side is a fundamental integer (of any cvref qualification). int_move , // A fresh `basic_big_int` is created because the left side is not movable. // The right side is a fundamental integer (of any cvref qualification). copy_int , // A fresh `basic_big_int` is created because the right side is not movable. // The left side is a fundamental integer (of any cvref qualification). int_copy , } ;
The benefit of such a design is that vast amounts of user code are sped up without any effort by the user, such as:
Since returns by value,
can repurpose the allocation of that result.
By comparison, if worked with ,
lots of allocations would be unnecessarily thrown away.
The downside of this design is that every binary operator needs to be a template
accepting forwarding references,
and this possibly applies to other future operations like as well.
However, this implementation complexity is not really perceived by the user.
If the operators were not optimal
,
the user would need to avoid overhead by writing:
It's obvious that the complexity of managing allocations optimally goes somewhere. The question is just whether it should be done invisibly by the standard library, or whether the burden is off-loaded onto users. We don't see a compelling reason to off-load onto users.
3.4.1. Why not return by && from unary operators?
For unary operators, it would also be possible to return by xvalue instead of by prvalue, but this would mean that the value category of the result of operators is not always the same. The perceived benefit is that an unnecessary extra object is avoided.
looks as follows:
It would also mean that users run danger of holding onto objects longer than they expected:
If refers to the original object ,
a data race could be introduced by working with on one thread,
and overwriting on another thread,
each of which is well-defined on its own.
Overall, this idea is making things more complicated and dangerous, with the only benefit being a micro-optimization which we don't see as sufficiently important.
3.5. Why not provide lower-level operations for arithmetic?
A common idea in the committee is that rather than providing the high-level construct
of ,
we could (first) provide the low-level building blocks,
such as multiprecision operations.
Then, any third-party library could easily build its own
type on top of such operations.
There is some merit to this idea;
our [Reference-Impl] internally dispatches to a number of functions
that operate on ,
and those could be exposed directly to the user.
However, we don't propose any of these low-level operations in this paper for a number of reasons:
-
The low-level operations are not actually useful in isolation
and not actually useful in a wide variety of domains.
Unlike other numeric operations like those in
, the multiprecision<cmath> operations are basically only useful for implementingspan .big_int -
Fixed-width multiprecision operations should already be covered by
, making the_BitInt ( N ) arithmetic functions even more limited in scope.span -
Many of the operations require additional allocations.
For example,
requires additional temporaryoperator / , unless the divisor has only one limb.basic_big_int -
does an enormous amount of work by dispatching to the proper operation depending onbig_int - type of operands (one may be of integral type),
- value category of operands (to repurpose allocations),
- limb count (different multiplication algorithms are chosen based on operand size),
- constant evaluation,
-
the availability of
(if a sufficiently large_BitInt ( N ) is available for the operation, the whole operation can be delegated to intrinsics by converting both sides to_BitInt ),_BitInt - […]
operation which does not have this vast amount of extra logic is not actually in their best interest. It would be like providing them with aspan function that doesn't handle infinities and NaNs and telling them that thissqrt is a lower-level building block. In reality, it's simply an incomplete and less useful version ofsqrt . Analogously, in the best case, theirstd :: sqrt operations would end up recreating the standard library implementation verbatim. In the worst (and much more likely case), the user'sbig_int would end up worse than the standard library version.big_int -
The design of these
operations is extremely complicated in itself. Among other important questions, it is unclear whether the operations should be templates that can takespan with both static and dynamic extent, how allocators should be passed (if at all), etc.span -
The motivation of this paper is largely based on
using
as a vocabulary type and as abig_int safe and dynamic
alternative to fixed-width integers. None of that motivates theseoperations.span -
There is no urgency to add
operations as a prerequisite forspan . If those low-level operations were ever added, the implementation ofstd :: big_int could simply start using them. There is no ABI break waiting to happen by standardizingstd :: big_int without thosestd :: big_int operations.span -
The
operations are a pessimization for constant evaluation. At least in theory, it is possible to makespan hold astd :: big_int or some otherstd :: meta :: info compiler internals handle type
which directly references the compiler's arbitrary-precision integer type. See also §2.5.std::big_int should be a compiler intrinsic for constant evaluation. Thecompiler handle approach
can result in much more lightweight constant evaluation, and only requires allocating when the object is transitioned from its compile-time representation to its runtime representation. By contrast, theoperations necessitate actually having aspan and managing its allocations, so more work is done in the frontend.span < uint_multiprecision_t >
Overall, we don't see a need for these operations in this paper.
They might be useful in spite of the problems listed above,
but are an entirely separate feature with separate motivation.
The explanation above also reveals that the premise of operations being lower-level
is simply wrong, to an extent.
The level is the lowest level at which approaches such as
wrapping a handle to is feasible.
The more is treated as a compiler intrinsic,
the lower-level it conceptually becomes.
3.6. Why no std::big_int_view ?
Similar to ,
one might imagine a (or view type with some other name).
This could even be seen as an easier first step towards multiprecision arithmetic.
However, we do not provide such a type for a number of reasons below.
3.6.1. Issues with std::big_int_view parameters
Remember that the point of as a parameter type
is to accept anything string-like.
It is also easy to make f callable for additional types by providing a user-defined conversion to std::string_view.
cannot provide similar functionality for integer literals.
Consider that we want the call below to be valid:
The key problem is turning the provided into and .
binds to a temporary object with value
(which is good because the lifetime of that object extends beyond the constructor call),
but we cannot reinterpret that as a .
We could still turn int a -like type
that can either hold
a or
a ,
but that does not solve the problem for large integers
like , , etc.
That requires at least an additional function:
3.6.2. Issues with std::big_int_view allocation management
The allocation that holds is actually
very useful for intermediate operations.
, since is an rvalue,
can reuse the allocation of for the result.
See also §3.4. Optimal operator overloads.
Also, thanks to having an rvalue overload for ,
allocations can be avoided using repeated in-place division by the base.
Similarly, when receives an rvalue ,
it shifts the limbs in-place as if by
if the allocation happens to be large enough.
All of this happens automatically and ergonomically.
Operations on a
would require the user to write more code to avoid allocations and unnecessary copies.
3.6.3. std::big_int_view is just not useful enough
The nail in the coffin is that the user cannot do much with a :
-
Not a lot of operations could be performed on a
without allocating astd :: big_int_view . Other than equality and relational comparison, almost any useful operation onstd :: big_int produces anotherstd :: big_int . Similar to how there is nostd :: big_int between twooperator + s, there shouldn't be anstd :: string_view between twooperator + s. To be fair,std :: big_int_view can at least providestd :: big_int_view andoperator - because we use §3.2.4. Sign and magnitude for the representation, so negation only requires flipping a sign bit in the container rather than modifying the limbs. However, this alone does not justify the existence of the type. By comparison,std :: abs provides the ability to search, check prefixes and suffixes, trim trailing/leading whitespace and other characters, create substrings in general, access random code units, etc., and these are all immensely useful non-allocating operations.std :: string_view -
Looking at the standard library,
there would be little benefit to having a
. Most operations we provide come with lvalue and rvalue overloads in order to repurpose the allocations of the arguments. A notable and rare exception isstd :: big_int_view , which only takes astd :: saturating_cast because the result is always a fundamental integer type.const & -
Third-party libraries typically do not provide a dedicated view type for big integers.
There is a
but noboost :: multiprecision :: cpp_int .boost :: multiprecision :: cpp_int_view
That being said, a type is not entirely useless;
it's just not useful enough to be included in this paper,
let alone be standardized before .
can always be provided later.
In the meantime, the rvalue overload of provides the same functionality
for mutable operands:
There is some evidence of user demand for such a negated view
operation.
GMP has a
parameter can be used to specify the sign.
3.7. Why no std::big_uint ?
We do not propose an unsigned counterpart to ,
nor any way to make unsigned.
unsigned big integers
to be provided in the first place.
Boost.Multiprecision does not allow
for arbitrary-precision .
When there is an arbitrary-precision integer built into the language, it tends to be signed-only, which has the concrete benefit of everyone using a single integer type.
One reason against providing is that doing so
contradicts the goal of providing safe and infallible operations
(not counting allocation failure).
Operations like subtraction, remainders, etc.
can produce negative values.
Saturating to zero is mathematically wrong
for and ,
so the most plausible way to handle the case is to throw an exception.
in Rust's
also contracts the goal of providing a vocabulary type.
Some users will inevitably use it for integers that can be large in value
but not negative,
which bifurcates the ecosystem into and .
Another observation is that unsigned integers become less motivated the larger the type is.
For 8-bit integers, this allows representing up to instead of ,
which is a meaningful difference.
When looking at §3.2.3.2. Layout in our reference implementation,
making unsigned would raise the maximum bit count from
137 billion bits to 274 billion bits.
Who is supposed to benefit from this?
To be fair, guaranteeing that the sign bit is always zero eliminates some runtime checks,
but even that is fairly limited.
For example, since uses a sign and magnitude representation,
most of the multiplication logic already ignores the sign.
The sign bit can simply be computed using an XOR at the end of the operation.
Overall, eliminating the sign bit is the pinnacle of micro-optimizations.
3.7.1. Burning the std::big_uint bridge
Not only is not part of this paper now,
we also don't leave any room for integrating it into
using the current set of template parameters.
That is, we don't have a for configuring signedness.
See also §3.8. Template parameters below.
parameter could be generalized to an ABI parameter
like:
However, this would be a fairly unprecedented change to an existing standard library type if made retroactively.
Even if the bridge was not burned,
adding in the form of extensions
would be problematic.
The status quo is that any code that uses can assume
that subtraction and negation of small values is non-throwing,
and that the only failure mode is producing stupidly large values
(with some rare exceptions like division by zero;
see also §3.15. Error handling).
Making throw or abort on negative values
would rug-pull these assumptions.
3.7.2. Conclusion
Providing would be in direct contradiction with our design goals,
so it is not provided.
The performance benefits are minimal.
Making possibly unsigned in the future is also problematic,
so we don't provide any reserved
template parameters which would enable that.
A big unsigned integer
type should either be provided immediately
(which we do not want)
or be an entirely separate .
3.8. Template parameters
3.8.1. Order of template parameters
For ,
the order is generally most likely to be specified as an argument first
:
The parameter needs to come after the parameter
so that the default argument can be spelled.
This is unfortunate because the limb type cannot be configured at all
(it must always be ),
but would not be so bad if it became genuinely configurable in the future.
Fortunately, the only people inconvenienced are power users who use
with a custom allocator,
which is acceptable.
is used to create fixed-width integers
(e.g. for 128-bit integers),
and we consider that use case to be obsolete
given [P3666R4]'s .
is thus also not relevant.
3.8.2. min_inplace_capacity
Consistently with the rest of the standard library
(e.g. , , ),
the type of is .
3.8.3. min_inplace_capacity restrictions
An interesting design question is whether
arguments should be disallowed
if the capacity is increased internally.
This might make sense because and
are functionally equivalent,
but distinct types with distinct instantiated code.
We decided not to restrict the argument because it may cause portability problems and disallows certain useful configurations.
.
However, if the minimum in-place capacity is 64 (even on 32-bit platforms)
in some particular implementation,
this clearly reasonable code would be disallowed.
Furthermore, it is very easy for the user to ensure no increase themselves:
alias template that would round up
like does,
without creating a distinct type.
3.8.4. Limb type parameter
When it comes to allowing configuration of the limb type, there is a spectrum of solutions:
- Don't allow any limb type configuration and have no template parameter. This is a simple design, but closes the door to future extensions forever because adding another template parameter would be an API break.
-
Ours:
Have a limb type parameter
but only allowclass Limb as an argument. This enables future extension.std :: uint_multiprecision_t -
Allow types as arguments that are
functionally equivalent
to. That is, they are distinct types that have the same object and value representation, likestd :: uint_multiprecision_t andunsigned long on 64-bit Linux.unsigned long long - Allow some subset of unsigned integer types, like 32-bit and 64-bit integers.
- Allow arbitrary unsigned integer types.
While we are not proposing any arguments other than
in this paper,
such future extension could be very reasonable for a few reasons:
-
Existing code that uses either
orunsigned long as a limb type would experience friction when working with spans ofunsigned long long , even if those types are functionally equivalent. For example, there would be no guarantee that an existing C API yielding astd :: uint_multiprecision_t to the user could obtain that pointer directly fromconst unsigned long * .std :: big_int :: representation ( ) - Some configuration of the limb type is reasonably implementable and quite useful. For example, 64-bit code interfacing with 32-bit legacy code bases could benefit from working with a 32-bit limb type.
-
Within a few decades, 128-bit arithmetic support may arrive on 64-bit platforms,
similar to how WASM32 supports 64-bit arithmetic operations
but has a 32-bit address space.
If so, there ought to be a way to create a
with 128-bit limbs, even ifstd :: basic_big_int cannot be redefined due to ABI concerns.std :: uint_multiprecision_t
Even if one doesn't find these arguments compelling,
is there strong enough reason to make it impossible to ever extend
with a different limb type?
Probably not.
The template parameter only mildly inconveniences implementers and power users
who don't merely use .
Most users work with
rather than ,
and we expect a similar pattern for vs. .
Thread attributes
,
in [thread.thread.class.general]:
functionally equivalent
unsigned integer types would ideally be done
with some compiler support or by ignoring certain instances of UB.
The underlying operations working with
could be blessed
so that internally,
e.g. can alias and .
This would allow having only one internal function while still handling all types.
3.9. Conversions
3.9.1. Converting constructor
We provide the following constructor for implicit conversions:
This allows:
-
An implicit conversion from any integer type.
This makes it possible to write functions such as
and call them likegcd ( std :: big_int , std :: big_int ) . The conversion should not be explicit because it would make initialization from literals too tedious, and the conversion is value-preserving anyway (not counting allocation failure).gcd ( x , 3 ) -
An explicit conversion from any floating-point type.
This could be implicit, but is not value-preserving.
See also §3.9.2. Explicit conversions to arithmetic types for more rationale.
Similar to division by zero and analogous to converting
tofloat , converting non-finite values results in undefined behavior. See also §3.15. Error handling.int -
An explicit conversion from any other specialization of
. This makes it possible to convert betweenstd :: basic_big_int with different allocators and SOO sizes, and make it possible to convertstd :: basic_big_int literals likestd :: big_int to other specializations. Similar constructors like the123 n constructor ofstring-view-like are traditionally alsobasic_string .explicit
The part ensures
that the constructor is if no allocation
ever needs to take place to represent the value.
to .
The is at least 32 bits,
so any can be represented without allocation,
and the converting constructor is .
Since floating-point types may have non-finite values and converting those to
results in undefined behavior,
conversions from floating-point types are never ,
consistent with the Lakos rule.
3.9.2. Explicit conversions to arithmetic types
is generally designed to mirror the interface of fundamental integer types,
though conversions are a notable exception.
For example, any integer type can be implicitly converted to ,
but makes this .
This follows the general design direction of more explicit conversions in new C++ types,
like → ,
non-value-preserving conversions in , etc.
Besides following an established design direction,
we also consider it surprising if one could convert a dynamically sized
which potentially holds thousands of bits to an ,
without ever expressing that intent in code.
There would also be no way to catch such conversions with list-initialization
since user-defined conversions are never narrowing.
3.9.3. Bit-precise integer interoperability
While [P3666R4] has not been merged into C++29 yet,
we still need to consider how will interoperate with .
Notably, the constructor of may or may not reject those integers,
and the user-defined conversion to integer types may or may not be valid.
We believe that this conversion should be valid in both directions because it exposes a
fundamental and clearly useful bit of functionality.
There is simply no good reason to prevent these conversions,
other than wanting to prevent from ever being a useful and fully supported
type in the standard library.
is specifically useful is for providing large integer literals.
For example, can be used instead of ,
which bypasses the user-defined literal,
and that UDL's implementation is relatively complicated and heavy.
As long as a sufficiently wide type exists,
its
at its library boundaries,
but performs the underlying calculations (such as for RSA) using e.g. .
This clearly requires converting to ,
and a user would need to perform that conversion manually limb-by-limb
if the standard library didn't provide it.
Such a conversion is also clearly useful when handing the values of
off to a C library that uses , , etc. in its API.
3.10. Expression templates (or lack thereof)
One possible direction is to provide expression templates for
operations, similar to Boost.Multiprecision (unless disabled).
That is, for example, would not return a object,
but rather a
which could be converted to a object when needed.
Expression templates enable two things:
- Avoiding unnecessary temporaries and allocations. This was a major motivation prior to C++11, and is pretty much irrelevant thanks to move semantics.
- Performing various mathematical optimizations.
Many transformations can be performed with expression templates, such as:
For fundamental integers,
the compiler typically performs these optimizations automatically,
but is too much of a black box for the compiler to reason about.
3.10.1. Technical reasons against expression templates
While it seems promising to have expression templates in order to perform these optimizations, we decided against this direction for a number of reasons:
-
The design becomes substantially more complicated.
Instead of every operation simply returning
, they now return an object representing the operation.basic_big_int -
These simplifications are a double-edged sword:
if the user doesn't spell out
, how can they be certain thatpow_mod ( x , y , m ) is equivalent? Will they remember months after having written the code that it is substantially transformed? Expression templates innately introduce complexity and uncertainty even though the user's code is simpler.pow ( x , y ) % m - Expression templates don't actually let the user express something new, they just transform expressions.
-
Putting the burden on the compiler is not free.
Instead of instantiation the
templates with justoperator @operands, they would be instantiated with all sorts of distinct expression template types.basic_big_int -
Since the return type isn't actually
, it can be problematic to pass the result into function templates or to store it inbasic_big_int variables, sinceauto doesn't deduce toauto . This regularly forces users to cast tobasic_big_int or to avoid the use ofstd :: big_int . The interaction withauto expressions is especially weird:auto no longer increments and copiesauto ( ++ x ) ; it copies thex object which is just a symbolic placeholder.big_int_expression
Implicit Evaluation of “auto” Variables and Arguments
proposed a solution to the problem,
so that even with expression templates,
would deduce to .
We do not endorse this proposal,
but it is worth mentioning.
3.10.2. The problem with optional expression templates
It should also be noted that expression templates are often an optional feature;
Boost.Multiprecision allows users to disable them
via the backend of the type.
For the C++ standard library,
making them optional this way is diametrically opposed to our goals:
we are trying to provide a single concrete vocabulary type in the form of ,
which is in conflict with having 50% of the users use a different type
based on their preference for expression templates.
On the other hand, making expression templates mandatory without an opt-out forces the aforementioned compile-time cost down everyone's throat.
explicitly,
they would pay for writing
with a distinct template instantiation.
That is because results in a distinct type representing the expression,
not in .
If both making expression templates optional and mandatory are bad options, perhaps the only remaining option is to not have them at all.
3.10.3. Expression templates in the ISO C++ process
Even if we decided that the benefits of expression templates outweigh the costs,
the ISO C++ process is not conducive to adding them.
Every single mathematical operation has some kind of interesting special forms
that could be transformed,
so any operation on would produce a new expression template type.
If the user actually wants to rely on these transformations,
they need to be standardized instead of being left up to the implementation,
which means that of the dozens of operations on ,
every combination with other operations needs to be investigated for special forms,
possibly leading to hundreds of these transformation rules.
If a C++ user discovers some interesting transformation, they would need to propose it to the committee, which is a perpetual maintenance burden for the committee. This also means that they won't get to use it until the next C++ standard is published and implemented, which means years of delay.
transformation
is discovered after the fact
.
By the time people's s are being turned into s automatically,
users have already been taught to write explicitly.
They might also keep using at that point
because it is more portable;
the transformation would only work in more recent standards
and might not yet be implemented in all standard library implementations.
3.11. User-defined literals for std::big_int
For convenient creation of ,
we provide the following user-defined literal:
The approach is the same as for other standard library UDLs,
such as .
3.11.1. Why 123n ?
The suffix ' is equivalent to .
3.11.2. User-defined literals vs. string constructors
It is worth noting that other libraries typically don't provide a UDL.
For example, Boost.Multiprecision instead has a string constructor,
expecting users to write .
We opted not to match that design because
-
The
syntax is more natural and concise than123 n .std :: big_int ( " 123 " ) -
The
syntax pessimizes runtime performance. The UDL approach allows pre-computing a" 123 " holding the limbs during constant evaluation. The runtime call toconstexpr uint_multiprecision_t [ ] only needs to copy the pre-computed limb array. Such a design is only possible thanks tooperator " " ( ) giving us the literal as a constant expression.template < char ... > - Third-party libraries avoid this form of UDL in part because it is very complicated to implement and requires parsing the integer literal with all features (including base prefixes, digit separators, etc.). In the standard, we can consider this to be an insignificant QoI issue. In the standard library implementation, we also have the luxury of doing the UDL parsing using a compiler intrinsic if it turns out to be too expensive to do in constant evaluation. Third-party libraries don't have this luxury.
Here, the compiler would do the work of parsing the literal and turning it into a limb array.
This would avoid turning into a template.
While this new form of UDL is not proposed here,
it still informs us that there is potential to improve UDLs and reduce some of their costs,
without breaking existing uses of .
This makes UDLs a more compelling option right now.
3.11.3. Why provide UDLs or string constructors at all?
Some form of convenient construction is necessary.
Otherwise, creating large constant objects is simply too tedious.
It would also be possible using the constructor
taking a ,
but this would require breaking any large constants up into individual limbs,
which is very difficult to do portably
considering that varies in size.
Another option is to rely on ,
but this feature is not yet available in the C++ standard,
and even if it was,
the is only guaranteed to be at least .
In other words, s larger than 64-bit are an optional feature.
3.12. Adjacent library utilities
There are a number of standard library utilities provided for fundamental integers,
such as and .
These are all useful for and
even if we didn't add the overloads in this paper,
someone would do it sooner or later.
We do not add any new integer utilities such as integer exponentiation or modular arithmetic; these can always be added later, and are brand-new orthogonal features that equally make sense for fundamental integers.
3.12.1. Hash support for std::big_int
We also provide object,
the result depends solely on the integer value
(i.e. the mathematical value represented) by the object.
This makes it possible to use a different as a key type
than the type used for lookup (assuming transparent lookup in some container).
However, the hash might not be compatible with that of fundamental types,
i.e. may be hashed differently than .
Also note that is already hashed differently from ,
so it's not possible to have hash parity with all integer types.
To maintain some degree of parity,
we ensure that as available,
equals
(where is the smallest possible width that can represent ).
3.12.2. std::numeric_limits specialization
There should be a specialization of
provided for .
Doing so is tradition in third-party libraries that provide their own infinite-precision integers,
and it provides useful capabilities such as writing generic code that works with
, , and ,
and distinguishing the infinite-precision integers via
.
3.12.3. std::in_range
We provide overloads for as follows:
While the same functionality can be achieved using
,
provides a more convenient and efficient way to check
if a value can be represented in another integer type.
The implementation of this check merely requires checking the sign bit and whether the limb count is 1 (on 64-bit platforms).
The other comparison functions in [utility.intcmp] are not overloaded
because this is not very useful for .
The comparison operators already provide the mathematically correct result in all cases.
We are also not convinced that much generic code would benefit;
functions are typically used when there is a known mismatch in signedness
between two bounded integers.
3.12.4. std::abs
We provide overloads for as follows:
This is an obviously useful overload that any user would expect to exist.
Header management provides a problem:
we don't want and to include ,
and both of these provide the overloads for other integer types.
To solve this, provides the overload
as well as the rest of the overload set
(not providing the rest of the overload set would risk ODR violations).
leaks through some other header,
the user included only and
their call to with fails,
but it's unclear how this problem could arise in practice.
Both and properly include ,
so the user gets all the associated functionality.
,
allocations can be avoided;
simply flips the sign bit of the container,
enabled by the §3.2.4. Sign and magnitude representation.
3.12.5. std::gcd , std::lcm , and std::midpoint
We provide , , and
overloads for as follows:
All of these take forwarding references to allow for optimal allocation reuse. They work the same way as the binary operators, as described in §3.4. Optimal operator overloads.
overload is notably different from that for fundamental integer types
because it allows different types for the two arguments.
This asymmetry is not nice,
but necessary to allow using as the left-hand side
and e.g. as the right-hand side.
Forcing both arguments to be of the same type would force a pointless allocation.
3.12.6. std::saturating_cast
We provide a overload for as follows:
… where is a signed or unsigned integer type.
This is partially motivated by the fact that Boost's
for behaves in a saturating
instead of truncating way.
While that behavior is useful,
it would be surprising to C++ users who expect conversions to truncate,
and it would be inconvenient in generic code that tries to handle
both and fundamental integer types.
In any case, providing provides an easy upgrade path
from to .
3.12.7. std::to_chars and std::from_chars
We also provide and
overloads for :
These are clearly useful.
A notable quirk is that both and
can potentially throw because they internally do arbitrary-precision arithmetic.
This is also why has an rvalue overload;
it allows reusing the allocation of for intermediate calculations.
However, should be guaranteed not to throw if is a power of two
because converting to base-two digits can be performed limb-by-limb in a single pass.
One could argue that providing is weird
because it operates on a fixed-size buffer,
while is dynamically sized.
However, most objects in practice could easily fit into a buffer
of a few hundred or thousand characters.
If so, the user should not be forced to go through to avoid an allocation.
3.12.8. Formatting support
is also formattable out of the box,
with , , etc.
Once again, this is clearly useful and expected by users.
The same options as for any fundamental integer type should be supported,
so that e.g. prints a in hexadecimal.
3.12.9. std::to_string and std::from_string
We also provide and
overloads for :
Since these are defined in terms of with a specifier,
the specification is trivial and the same as for any fundamental integer type.
In practice, an implementation would directly call to
cut out the middle-man
between and .
has an rvalue overload so that the allocation of
can be reused for intermediate calculations.
and also need such an overload
to avoid pessimization.
3.12.10. No <bit> support
While bit-manipulation functions are useful for ,
these are not included in the proposal because there are too many design problems
that need to be resolved,
and this should better be done in a follow-up paper:
-
Some of the functions such as
don't really make sense for an infinite-precision integer type. It is unclear which ones to provide.std :: rotl -
While
andstd :: countr_zero are useful, there would need to be error handling for the case ofstd :: countr_one andstd :: countr_zero ( 0 n ) , since the result isstd :: countr_one ( - 1 n ) infinity
. -
uses<bit> to report results such as inint , but this seems insufficient forint std :: popcount . That opens the question whether to use a fixed type likestd :: big_int or some implementation-defined signed integer type, and whether that type should have a spelling exposed to the user.long long -
It is unclear how
should work forstd :: bit_repeat . The function takes astd :: big_int pattern andT pattern length and repeats that pattern as many times as possible. Forint , that would result in an infinite amount of repetitions. Nonetheless,std :: big_int would be useful forstd :: bit_repeat ; it just needs a different design that is not yet clear.std :: big_int -
The
header currently accepts only unsigned integers in most functions, but<bit> is signed, seemingly contradicting the current design direction.std :: big_int -
While
could be useful forstd :: bit_reverse , it's unclear how to define it for negative numbers.std :: big_int
3.13. Naming
The name was chosen because it meets user expectations
and makes its design instantly clear.
In languages where infinite-precision integers are not built-in,
the name is virtually always some variant of big int
or big integer
.
The name is also immediately recognizable
as supporting infinite-precision arithmetic and as growing elastically as needed.
Previous proposals used the name ,
but this name doesn't convey the design well,
and is too similar to concept names like .
3.14. Choice of header
We propose a new header that just provides
and its utility functions.
While could belong in ,
that header is already enormous,
and is essentially a standalone container.
It could be seen as a with some operator overloads.
Containers traditionally have their own headers, like .
3.15. Error handling
has elastic operations,
meaning that it grows as needed to fit the result,
making many operations infallible.
The allocations can still fail and throw ,
same as for any other container.
Fallible operations include
-
andoperator / , if the divisor is zero,operator % -
conversion from
tonumeric_limits < float > :: infinity ( ) , andstd :: big_int -
andoperator << with negative shifts.operator >>
We handle this as undefined behavior.
From a runtime cost perspective,
this is unjustified because already cannot be minimal and entirely branchless
due to the §3.2.3. Small object optimizations,
even if there is no extra check for division by zero.
division is also a fairly heavy operation,
so the general principle of don't harden high-performance, low-level numerics
doesn't really apply here.
The reason we still have undefined behavior for these cases is that there is no clear precedent
for error handling in heavy numerics
in the standard library.
offers some precedent,
but arguably goes too far by throwing on runtime checks even in fast and low-level operations
like ,
making the type less attractive for some high-performance use cases.
The proposed undefined behavior can be thought of as a placeholder,
and undefined behavior can always be filled in
with something better at a later point.
4. Future direction
The initial proposal is only the start.
There are many features that could be added subsequently
but which are not in this paper to keep its scope focused.
4.1. Integer numeric functions
This paper only provides overloads for pre-existing functions,
such as .
However, infinite-precision libraries typically provide many more operations, such as
- modular arithmetic functions, like
,powmod - integer exponentiation (
),ipow - integer logarithm (
),ilog - integer square root (
),isqrt - factorial (
),fact - binomial coefficient (
),choose - […]
The key observation is that these functions are at least as useful for fundamental integer types,
not just for ,
and they should be designed for integers in general.
That makes all of them separate and orthogonal features,
where a overload is just one of the design aspects.
type in JavaScript was standardized in such a minimal form as well:
only the numeric type itself is provided,
while the additional utilities are largely missing.
4.2. Fixed-point interoperability
To provide interoperability with fixed-point numbers, a conversion to/from such numbers could be provided. Concretely, this would require
- a constructor taking the integer and its scale (in bits),
- a conversion function that yields the integer and its scale.
Neither is truly necessary because
conversion from fixed-point could also be implemented in terms of
construction from an integer followed by or .
conversion to fixed-point could also be implemented in terms of a
overload for and in terms of .
However, more direct implementations may provide some convenience.
In any case, there is no urgent need to provide such convenience in this proposal, especially considering that there are zero-overhead workarounds using bit-shifting.
4.3. Random number generation
Adding support for in is another logical next step,
but out of scope in this paper in order to limit the scope.
Presumably, the approach would be to allow in
and other facilities.
4.4. Making std :: uint_multiprecision_t C-compatible
As mentioned in §3.2.2. C compatibility,
it may be beneficial to improve C compatibility
by putting in ,
and possibly by standardizing the type for C2y.
4.5. Non-transient allocations
would benefit from non-transient allocations
because it would allow variables to hold any integer value.
Currently, the limit is imposed by the .
However, this is a general problem, and any solution
should include , , etc.
Recent proposals dealing with this problem include:
4.6. Constant template parameters
While 's implementation doesn't meet the requirements
of a structural type,
it could be declared magically structural
in the standard
or it could be made usable as a template parameter in some other way.
Using some compiler support,
the name mangling could be customized so that becomes usable
as a template parameter.
That is, could be mangled as is mangled as
However, again, this is is a general problem,
and other types such as could benefit from being structural as well.
Most recently, [P4340R0] explores solutions
that would make further class types usable as template parameters.
5. Arguments against standardization
While there is strong motivation to include in the standard,
there are also some arguments against doing so, which need to be considered carefully.
5.1. Many trade-offs
is relatively complex,
and any complex design comes with a number of trade-offs.
This does provide some motivation against standardization.
Generally speaking, if a feature has so many trade-offs that most
users will have to make their own form of a feature,
this suggests that the feature is unfit for standardization.
Some concrete trade-offs for (and our response to these) are:
| Trade-off | Explanation | Our response |
|---|---|---|
| SOO | We force §3.2.3. Small object optimizations, which does pessimize the use case where the values are always huge. |
|
| SOO size | Depending on the use case, different amounts of small object optimization are appropriate. |
We solve this by making the amount configurable like in .
|
| Allocation | Different use cases require different forms of allocation. |
We address this by making allocator-aware.
|
| Constant-time operations |
In certain applications such as in cryptography,
constant-time operations are desired, and
is not designed around constant execution time.
|
|
| Representation | The representation could be §3.2.4. Sign and magnitude, two's complement, or anything else. The choice depends on what is most convenient for the surrounding code. |
In theory, any representation could be chosen,
but for reasons explained in the referenced section,
this is almost always sign and magnitude.
The choice largely boils down to minimizing the cost of operations.
Two's complement types like are a rare exception.
|
| Elasticity |
operations are elastic
(meaning growth happens automatically to fit the result),
but some use cases require inelastic results,
like .
|
This is arguably not a trade-off because inelastic operations can always be added later,
via e.g. or functions.
The trade-off is limited to the fact that those operations would not simply be spelled
.
|
| Error handling |
is entirely non-throwing,
except for exceptions thrown during allocation failure
or as the result of undefined behavior
(e.g. division by zero in ).
|
|
In summary, the idea that has too many domain-specific trade-offs
is largely unsubstantiated.
Allowing configuration of the and allocator
provides sufficient flexibility for virtually every use case.
Any remaining trade-off in the design of would be mildly annoying at worst.
type;
there will always be people creating their own , their own ,
etc. because their use is simply too specialized for the standard library.
Some people even make their own standard library.
However, our is good enough to be everyone's default
,
which is good enough for any standard feature.
Another way to look at this issue is to consider whether
is suitable for each of the advertised §2.1. Use cases
(serialization, safety, scripting engines, statistics, scientific computing, and cryptography).
For each of these, it is possible to imagine a reason to avoid ,
like not being optimized with manual assembly for some architecture used in scientific computing,
but remains an acceptable default choice for most users;
there is no deal breaker
among the trade-offs above
that would make it unsuitable for the domain.
5.2. Use of committee resources
Another concern with is that it undoubtedly consumes
committee resources both to standardize initially,
and to maintain.
This is naturally the case with any large feature.
However, big integer types have many millions of uses throughout various programming languages already, and standardization will likely make that number skyrocket for C++, so this is simply committee time well-spent, however much time it is.
5.3. Increased complexity of future integer features
The current design approach of is to closely imitate
the fundamental integer types.
Rather than providing operations as member functions,
existing standard library facilities like and
should be extended to support
in addition to signed and/or unsigned integer types.
Consequently, once a decent amount
of coverage for is provided,
the expectation is that any new integer utility would also support .
This is very similar to how any new functions in or
are now expected to have overloads,
for the sake of consistency.
We don't consider this permanent increase in standardization effort
to detract from because
-
In a lot of cases,
those new integer utilities are a useful part of big integer libraries anyway.
For example, those libraries typically come with integer exponentiation functions,
so
support for such a newstd :: big_int function would be added enthusiastically, not reluctantly. See also §4.1. Integer numeric functions.std :: ipow -
It is quite easy to design the function signatures for new
utilities once there are some examples in the standard library setting a precedent.std :: big_int -
A naive implementation is often quite easy to provide for
, by simply doing the same as for the fixed-width integers. This is very similar to how the naive implementation forstd :: big_int is to perform the scalar operation elementwise.simd :: vec
In short, the added cost to new integer utilities is either low or effectively zero
(if we would have added support anyway,
it's not really an additional cost).
5.4. Use of implementer resources
Another similar criticism is that would consume a substantial amount
of implementer resources,
especially when the goal is to have a high-performance implementation.
There are many factors that address this issue:
-
Standardization of the type concentrates the work of third-party libraries in
the standard library.
Many upgrades to e.g. Boost.Multiprecision might also be
backported
to the standard library counterpart, and vice versa. - Due to how ridiculously widespread big integers are in C++ already (see GitHub code search in §1. Introduction), standardization would act as an invite to outside collaborators.
-
Every major compiler vendor already provides a big integer type,
such as
. While some major refactoring would be required, vendors could end up converting these types into wrappers aroundllvm :: APInt or could at least implement the numeric operations viastd :: big_int . For example,std :: big_int is the spiritual equivalent of allvm :: APIntOps :: pext overload forstd :: bit_compress , so why maintain both?std :: big_int
The biggest remaining concern is that standardization trifurcates the implementations into
a libstdc++ ,
a libc++ , and
an MSVC .
However, this work is already trifurcated among the vendors' existing big integer types (see above),
so standardization does not create additional duplicated effort
so much as relocate this existing work for the benefit of users.
5.5. Residual fragmentation
A possible criticism of is that
everyone already has their own big integer
.
Or more precisely, the ecosystem is already full of solutions
such as GMP, Boost.Multiprecision, etc. or various internal implementations
of multiprecision used in cryptographic libraries.
Even if was standardized,
the ecosystem wouldn't simply drop their own solutions overnight.
While that is true,
these arguments shouldn't stop any proposal on their own.
Any new feature takes time to adopt,
and even if it provides a better or more convenient solution to a problem,
it's unrealistic for everyone to rewrite their existing solutions.
Similarly, the addition of C++17
does not mean that everyone would instantly move away
from existing solutions like ,
but that doesn't make C++17's new type worthless.
5.5.1. N-to-N conversion problem
Furthermore, there is an N-to-N conversion problem between all these existing solutions.
How does one convert to e.g. OpenSSL's ?
The most universal form of conversion is to convert to a string and parse as another type,
but this is comically expensive and involves multiprecision division operations.
provides at least an intermediate representation that turns this into a 1-to-N
conversion problem.
That is, any existing big integer type can be extended with a constructor taking
or
to facilitate conversions,
and any library with some existing internal integer type can at least provide new functions
taking such parameters in its top-level API.
5.6. Inability to change ABI
Since the standard library is typically kept in an ABI-stable state,
standardizing has the downside of losing some potential flexibility
compared to third-party libraries.
A crucial question is thus whether frequent changes to the representation
of are plausible and desirable,
i.e. whether we forfeit much in this regard.
We believe this is either not the case or can be mitigated with our design, for the following reasons:
-
The SOO size can be customized with the
parameter, so even if a user realizes that themin_inplace_capacity layout is not suitable for them, they can choose anotherstd :: big_int .std :: basic_big_int - The overall sign-and-magnitude layout is an industry standard that has not seen meaningful change in the last decades. Most big integer implementations use this layout.
-
Even more fundamentally,
the approach of performing multi-precision operations using limb arrays is an eternal standard.
For over 50 years, CPU instructions like
adc have been specifically designed around performing multi-precision operations word-by-word, and arranging words in an array has always been the obvious layout to make this seamless. -
The layout of Boost.Multiprecision's
has seen little change historically.boost :: multiprecision :: cpp_int -
For really large integers (multi-limb case),
the underlying operations work with
, and the details of how those limbs are laid out in the container are somewhat irrelevant.span < uint_multiprecision_t >
In conclusion,
while the ABI freeze
is a real concern with ,
the ABI is fairly stable in practice,
for various innate reasons.
However, it is plausible that some details of how §3.2.3. Small object optimizations
are implemented may be imperfect at first,
and tweaking those details would be difficult in the standard library.
5.7. Header bloat
The design strategy for is to provide supporting utilities as overloads
of existing standard library functions.
That is, , , etc.
receive new overloads accepting .
An unfortunate consequence is that this bloats any and all integer utilities with
support,
including formerly lightweight headers like .
To be fair:
-
We have never really kept our headers minimal,
with some extreme example like including
for<memory> and getting all ofstd :: to_address ,std :: atomic , andstd :: shared_ptr algorithms.uninitialized_ * -
None of this is a problem with
.import std ; -
is still relatively lightweight. It manages its representation similar to<big_int> , and the really complicated numeric parts like Toom-Cook multiplications live in the runtime library anyway. Thestd :: vector paths use a much smaller implementation.constexpr
6. Implementation experience
Our reference implementation can be found at [Reference-Impl].
6.1. Deployment experience
This reference implementation is still very recent
and has no real-world deployment experience.
That being said, is heavily modeled after Boost.Multiprecision
and has decades of deployment experience
across hundreds and thousands of projects.
6.2. Benchmarks
See https://eisenwave.github.io/std-big-int/benchmarks.html.
Overall, our implementation is typically a bit
slower than (Boost wrapper around GMP) and
on par with
on identical generic driver code
(that is, templates that perform computation for any of these integer types).
These benchmarks are unsurprising:
GMP uses hand-written assembly optimized over many years and
represents somewhat of a theoretical optimum,
and our top-level API and implementation internals are so similar to those of Boost,
not much should be gained or lost in terms of performance.
7. Wording
The changes are relative to [N5032].
[headers]
In [headers], add a new element to C++ library headers table:
[version.syn]
Add a feature-test macro to [version.syn] as follows:
[numeric.limits]
[numeric.limits.general]
Change [numeric.limits.general] paragraph 5 as follows:
Non-arithmetic standard types, such as ([complex]),
shall not have specializations,
except for specializations of ([big.int]).
[numeric.special]
Insert a new paragraph at the end of [numeric.special] as follows:
The specialization for ([big.int])
shall be provided as follows:
[utility]
[utility.syn]
Change the synopsis [utility.syn] as follows:
[utility.intcmp]
Change [utility.intcmp] as follows:
[…]
8
Mandates:
Each of and
is a signed or unsigned integer type ([basic.fundamental]).
9 Effects: Equivalent to:
10
Mandates:
is a signed or unsigned integer type ([basic.fundamental]).
11
Returns:
.
12 Complexity: Constant.
.
is actually querying whether the number of bits
currently used to represent the integer value
is less than or equal to the the width of .
See also §3.12.3.
[numeric.ops.overview]
Change the synopsis [numeric.ops.overview] as follows:
[numeric.ops.gcd]
Append a new item to [numeric.ops.gcd] as follows:
Effects: Equivalent to:
Complexity:
,
where and are the representation sizes
of and , respectively.
[numeric.ops.lcm]
Append a new item to [numeric.ops.lcm] as follows:
Effects: Equivalent to:
Complexity:
,
where and are the representation sizes
of and , respectively.
[numeric.ops.midpoint]
Append a new item to [numeric.ops.midpoint] as follows:
Effects: Equivalent to:
Complexity:
,
where and are the representation sizes
of and , respectively.
[numeric.sat.cast]
Change [numeric.sat.cast] as follows:
template < class R , class T > constexpr R saturating_cast ( T x ) noexcept ; 1 Constraints:
andR are signed or unsigned integer types ([basic.fundamental]).T 2 Returns: If
is representable as a value of typex ,R ; otherwise, either the largest or smallest representable value of typex , whichever is closer to the value ofR .x
3
Constraints:
is a signed or unsigned integer type ([basic.fundamental]).
4
Returns:
If the integer value ([big.int.class]) of
is representable as a value of type , ;
otherwise, either the largest or smallest representable value of type ,
whichever is closer to .
5 Complexity: Constant.
[numeric.int.div]
There is a proposal [P3724R3] in the pipeline which adds the initial set of functions.
[string.syn]
Change [string.syn] as follows:
[string.conversions]
Change [string.conversions] as follows:
[…]
Returns:
.
Returns:
.
Returns:
.
Postconditions:
is in a valid but unspecified state.
.
Simply passing into wouldn't do anything
because formatting does not handle rvalues specially.
It would also be possible to specify in terms of directly
(which more closely matches the actual implementation),
but this is much more complicated and should probably be done consistently
for the pre-existing overloads too.
[…]
Returns:
.
Returns:
.
Returns:
.
Postconditions:
is in a valid but unspecified state.
[…]
[big.int]
In Clause [numerics], insert a new subclause immediately following [complex.numbers].
X Arbitrary-precision arithmetic [big.int]
[big.int.general]
X.1 General [big.int.general]
1
The header defines a class template for performing
arbitrary-precision integer arithmetic,
as well as related type aliases.
2
Throughout subclause [big.int],
Complexity: elements for operations refer to the amount of operations
performed on objects.
are not observable,
so measuring complexity in terms of those is wishy-washy.
However, there is no real alternative
that would let us measure complexity for e.g. ,
where it cannot be measured in terms of allocation size,
since nothing is allocated.
[big.int.syn]
X.2 Header <big_int> synopsis [big.int.syn]
uint_multiprecision_t basic_big_int big_int 1
The type alias
denotes a standard unsigned or extended unsigned integer type ([basic.fundamental])
which has no padding bits.
2
Recommended practice:
should be chosen
to have the greatest possible width
so that an arithmetic expression ([expr.pre])
performed on operands of the type
corresponds to a single instruction in the execution environment.
[big.int.class]
X.3 Class template basic_big_int [big.int.class]
1
The class template describes arbitrary-precision, signed integer types.
Objects of such types may be capable of representing much greater integers
than signed integer types ([basic.fundamental]).
Arithmetic operations on these objects may dynamically allocate memory to fit the result.
2
The program is ill-formed if template parameter
is not .
3
A represents an integer value;
the integer value of an object of integral type
is the value ([basic.types.general]) of that object.
The magnitude of the integer value of a is either
represented using subobjects nested within a or
represented within storage obtained from the given ;
the sign of the integer value is represented separately.
4 The effective width of an integer value is the width of the smallest hypothetical unsigned integer type ([basic.fundamental]) able to represent the magnitude of .
5
Template parameter specifies the minimum width of integers
that a can represent
using a subobject nested within.
shall be nonzero and
less than or equal to an implementation-defined limit.
That limit shall be greater than or equal to the maximum width
of any integer type ([basic.fundamental]).
as well.
That is, is able to represent any value without allocations,
if the user specifies a sufficiently large .
In practice, this is trivial to implement because simply specifies
the size of the array inside .
[big.int.require]
X.3.1 General requirements [big.int.require]
1
If any operation would cause to exceed ,
that operation throws an exception object of type .
2
If any member function or operator of throws an exception,
that function or operator has no other effect on the object.
3
Every object of type
uses an object of type to allocate and free storage
for the contained objects as needed.
shall meet the Cpp17Allocator
requirements ([allocator.requirements.general]).
4
The representation of a object
is the sequence of elements
that collectively represents the integer value of that object.
Unless otherwise stated,
-
any member function or member function template
without
qualifier, andconst -
any free function or specialization of a function template
that has a parameter of type
rvalue reference to
basic_big_int
described in [big.int] invalidates
the representation of the object,
meaning that results previously returned by
are no longer valid.
Whenever a object is left in a valid but unspecified state,
its representation is considered invalidated.
5
Any function that has a parameter
and any member function of
with a
6
During constant evaluation,
if the effective width of the integer value of a
is less than or equal to its member,
the object holds no allocation following any operation.
[Note:
The behavior is as if were called
following every operation that may allocate.
— end note]
[big.int.defns]
X.3.2 Types and constants [big.int.defns]
1
The value of the static data member
is the amount of
nested within a object
and which participates in representing its integer value.
2
Remarks:
The value of shall be at least
.
3 Remarks: The instantiation is ill-formed if the multiplication is not the mathematical product of the two factors.
is representable in ,
but the multiplication leads to overflow/wrapping.
It effectively imposes a limit on the user-provided .
[big.int.expos]
X.3.3 Exposition-only helpers [big.int.expos]
1
The exposition-only concept
is satisfied and modeled if and only if
is a signed or unsigned integer type ([basic.fundamental]).
2
The exposition-only concept
is satisfied and modeled if and only if
is either a signed or unsigned integer type ([basic.fundamental])
or a specialization of .
3
The exposition-only concept
is satisfied and modeled if and only if
is either a cv-unqualified arithmetic type ([basic.fundamental])
or a specialization of .
4
Let be , and
let be .
5 Result:
-
If
andLT are the same specialization ofRT ,basic_big_int ;LT -
otherwise, if
is a specialization ofLT andbasic_big_int is a signed or unsigned integer type ([basic.fundamental]),RT ;LT -
otherwise, if
is a specialization ofRT andbasic_big_int is a signed or unsigned integer type ([basic.fundamental]),LT ;RT - otherwise, the type alias is ill-formed.
6
Effects:
is if
is a signed or unsigned integer type
whose width is less than or equal to ,
and otherwise.
7 Let:
beLT .remove_cvref_t < L > beRT .remove_cvref_t < R > -
be a hypothetical signed integer type with sufficient range to represent the integer values ofT ,x , and ofy .f ( static_cast < T > ( x ) , static_cast < T > ( y ) ) -
bep orx , chosen as follows:y -
If exactly one of
orLT is a specialization ofRT ,basic_big_int isp orx , respectively.y -
Otherwise, if exactly one of
orL is not an lvalue reference,R isp orx , respectively.y This covers the case where there is exactly one given rvalue, so its allocation should be reused rather than unnecessarily copying.basic_big_int -
Otherwise, it is a property of the implementation whether
isp orx .y This covers the case where both operands are rvalues, so perhaps the larger pre-existing allocation should be used, or for simplicity, always the left one. This is not implementation-defined so that there is no documentation requirement for such a detail.
-
If exactly one of
8
Returns:
A object
whose integer value is that of and
whose allocator is obtained from .
9
Remarks:
If is a specialization of ,
is left in an unspecified but valid state.
If is a specialization of ,
is left in an unspecified but valid state.
If is not left in an unspecified state,
the allocator of the result object is initialized from
.
[Note:
Both operands can be left in an unspecified state.
— end note]
utility is needed for all sorts of operations,
such as binary operators and and .
The handling of allocators is similar to
between s ([string.op.plus]).
In the case of two rvalue operands,
both sides are left in a valid but unspecified state,
which gives the implementation the freedom to choose either allocator.
Other than needing to use for copying,
the initialization of the allocator is left up to the implementation.
This means that in the rvalue case,
the allocator may be initialized via like in the move constructor,
but may also be copied.
[big.int.cons]
X.3.4 Construct/copy/destroy [big.int.cons]
1 Effects: Initializes the integer value to zero. Value-initializes the allocator.
2 Complexity: Constant.
3
Effects:
Initializes the integer value to zero.
Initializes the allocator to .
4 Complexity: Constant.
5
Effects:
Initializes the integer value to that of .
Initializes the allocator to
.
mirrors container requirements
imposed in [container.requirements].
6
Throws:
Nothing if the effective width of the integer value of
is less than or equal to ;
otherwise, exceptions thrown during allocation.
7
Complexity:
Linear in the size of the representation of .
8
Effects:
Initializes the integer value to that of .
Initializes the allocator to .
mirrors container requirements
imposed in [container.requirements].
9 Complexity: Constant.
10
Effects:
Initializes the integer value to that of .
Initializes the allocator to .
11
Complexity:
Linear in the size of the representation of .
12
Constraints:
is .
with other allocators
or with the same allocator but different .
13
Preconditions:
If is a floating-point type,
the value of is finite.
14
Effects:
If is an integral type or
a specialization of ,
initializes the integer value to that of .
Otherwise, is a floating-point type,
and this object is initialized to the integer value obtained
by discarding the fractional part of .
15
Throws:
Nothing if the effective width of the integer value
this object is initialized with
is less than or equal to ;
otherwise, exceptions thrown during allocation.
16
Remarks:
The constructor is explicit if is neither
a signed or unsigned integer type ([basic.fundamental]) nor
the current specialization of .
specializations with other allocators,
but to make allocator mixing and floating-point conversions explicit.
Also explicit is the conversion from character types to ,
which is arguably needed because character types and integers are used in different domains.
17
Preconditions:
If is a floating-point type,
the value of is finite.
18
Effects:
If is an integral type or
a specialization of ,
initializes the integer value to that of .
Otherwise, is a floating-point type,
and this object is initialized to the integer value obtained
by discarding the fractional part of .
Initializes the allocator to .
19
Throws:
Nothing if the effective width of the integer value
this object is initialized with
is less than or equal to ;
otherwise, exceptions thrown during allocation.
20
Effects:
Initializes the integer value to an integer value formed
by concatenating the base-2 representation of each element
in the range ,
where the first element in that range holds the least significant part
of the concatenated base-2 representation.
If is a signed type,
the combined base-2 representation is interpreted as that of a signed integer,
otherwise as that of an unsigned integer.
Initializes the allocator to .
21
Throws:
Nothing if the effective width of the combined integer value
is less than or equal to ;
otherwise, exceptions thrown during allocation.
22
Complexity:
Linear in the size of .
23
Effects:
Equivalent to:
.
[big.int.ops]
X.3.5 Operations [big.int.ops]
1
Returns:
A representing the range of digits either
nested within this object or dynamically allocated,
where the first digit in the range has the least significant set of bits.
The of the result is .
is added by [P3724R3].
I would expect it to be available by the time is standardized.
2 Complexity: Constant.
3
Remarks:
If the integer value is greater than or equal to zero,
has the same integer value;
otherwise,
has the same integer value.
[Note:
Consequently, elements of type
that are part of the representation must be kept in the correct state,
including otherwise extraneous upper bits of magnitude greater than the integer value.
This restriction does not apply to elements that are allocated
but not part of the representation.
— end note]
-
needs to store abasic_big_int of dynamically allocated data and ofunion to make the value accessible viauint_multiprecision_t .span - The sign bit is kept separate.
- The padding needs to be kept zero.
4
Returns:
If the integer value is zero, ;
otherwise
,
where is the integer value.
for
a hypothetical signed integer type with infinite range and
a hypothetical unsigned integer type with infinite range.
However, this description seems inelegant.
It would also be possible to imitate the wording from [bit.pow.two],
but with the addition of abs
/magnitude
,
we are describing too complicated a math formula in prose.
5 Complexity: Constant.
6
Returns:
If the integer value is zero, ;
otherwise
.
7 Complexity: Constant.
8
Returns:
.
9 Complexity: Constant.
10
Returns:
The maximum number of objects
that can be part of the representation.
The result is greater than or equal to and
sufficiently low for to be the mathematical product of
and
.
11 Complexity: Constant.
12
Returns:
.
13 Complexity: Constant.
14
Returns:
,
where is the number of currently allocated
objects.
15 Complexity: Constant.
16 Returns: The allocator of this object.
17 Complexity: Constant.
18
Effects:
A directive that informs a of a planned change in size,
so that the storage allocation can be managed accordingly.
Reallocation happens at this point if and only if the current capacity is
less than the argument of .
19
Postconditions:
is
greater or equal to the argument of if reallocation happens; and
equal to the previous value of otherwise.
20
Effects:
Equivalent to:
21
Effects:
If the effective width of the integer value is
less than or equal to ,
frees the allocation and stores the integer value
within the object.
Otherwise, is a non-binding request
to reduce to .
It does not increase ,
but may reduce causing reallocations.
22 Complexity: If the size is not equal to the old capacity, linear in the size of the sequence; otherwise constant.
[big.int.modifiers]
X.3.6 Modifiers [big.int.modifiers]
1
Effects:
Sets the integer value to that of .
If
is ,
assigns to the allocator of this object.
2
Returns:
.
3
Complexity:
Linear in the size of the representation of .
4
Effects:
Sets the integer value to that of .
If
is ,
assigns to the allocator of this object.
5
Returns:
.
6 Complexity: Constant.
7
Constraints:
is .
8
Effects:
Sets the integer value to that of .
9
Returns:
.
10 Effects: Equivalent to:
where @ is a placeholder for the token
in the respective .
11
Effects:
Equivalent to:
12
Effects:
Equivalent to:
13
Effects:
Exchanges the integer values of this object and of .
If
is ,
exchanges and the allocator of this object.
14 Complexity: Constant.
[big.int.conv]
X.3.7 Conversions [big.int.conv]
1 Let be a prvalue equal to the integer value of this object, of a hypothetical signed integer type with sufficient range to represent .
2
Constraints:
is a cv-unqualified arithmetic type ([basic.fundamental]).
3
Returns:
.
[Note:
If is ,
the result is if has nonzero integer value and
otherwise ([conv.integral]).
If is a floating-point type,
the result value is determined as if by floating-integral conversion ([conv.fpint]).
— end note]
[big.int.unary]
X.3.8 Unary operations [big.int.unary]
are worded as equivalences
because we want to inherit everything (including complexity requirements)
from constructors.
1
Effects:
Equivalent to:
2
Effects:
Equivalent to:
and are worded only as Returns:
because the actual implementation strategy for negation is usually
to perform a copy with the sign bit being flipped at the same time.
For complement, it is to perform a flip of the sign bit and an increment of the magnitude, or the other way around, and we don't want to impose any particular order on these two steps.
3
Returns:
.
4
Complexity:
Linear in the size of the representation of .
5
Returns:
.
6 Complexity: Constant.
7
[Note:
The contents of the representation of the result
are identical to those of prior to the call.
— end note]
8
Returns:
.
9
Complexity:
Linear in the size of the representation of .
10
Returns:
.
11
Complexity:
Linear in the size of the representation of .
and are worded as equivalences
because this inherits everything.
Postfix operators are defaulted.
12
Effects:
Equivalent to:
13
Effects:
Equivalent to:
[big.int.alias]
X.3.9 Alias big_int [big.int.alias]
1
Result:
A specialization of with an implementation-defined
argument for the constant template parameter,
chosen so that
is greater than or equal to and
equals .
2
Recommended practice:
should be sufficiently large so that
may represent the value of all commonly used integer types without allocating.
is typically on a 64-bit architecture,
and it cannot be any less because it needs to be at least one limb width.
However, an implementation could also decide to choose , , etc.
There is a trade-off between increasing container size and the need to allocate more.
[big.int.cmp]
X.3.10 Non-member comparison operator functions [big.int.cmp]
1
Returns:
if the integer value of
is equal to the integer value of , and
otherwise.
2
Complexity:
Linear in the minimum of the representation sizes of and .
3
Returns:
if the integer value of
is less than the integer value of ,
if the integer value of
is greater than the integer value of , and
otherwise.
4
Complexity:
Linear in the minimum of the representation sizes of and .
requirement means that it's not a valid implementation strategy
to wrap any integer in because that may allocate.
Instead, either the integer value or each limb
must be compared with the other object.
This may involve multi-precision comparisons such as in .
[big.int.binary]
X.3.11 Binary operations [big.int.binary]
1
Preconditions:
For and ,
the integer value of is nonzero.
2
Returns:
For each operator function template
where @ is a placeholder for the token in the respective ,
equivalent to:
[Note: Bitwise operations are performed as if on a two's-complement representation, where positive numbers have an infinite number of leading zeroes, and negative numbers have an infinite number of leading ones. — end note]
3
Let be the representation size of and
let be the representation size of .
4
Complexity:
For , ,
, , and ,
.
For , , and ,
.
5
Let @ be
for the first overload and
for the second overload.
6
Constraints:
is a specialization of .
7
Preconditions:
is greater or equal to zero.
8 Effects: Equivalent to:
9
Complexity:
Linear in the size of the representation of .
[big.int.hash]
X.3.12 Hash support [big.int.hash]
1 The specialization is enabled ([unord.hash]).
2
Remarks:
Let be an object of type
, and
let be an object of type
.
If and
have equal integer value ([big.int.class]), then
equals
.
[Note:
For an object of integral type ,
can be unequal to .
— end note]
is not equal to ,
so hash parity with all integral types is not possible.
3
Let be an object of type
,
and let be the width of the smallest hypothetical signed integer type
that can represent the integer value of .
If the implementation provides a type
to provide compatibility with type defined in ISO/IEC 9899:2024,
then
equals
.
hashing
.
is the same as casting to and hashing that
[big.int.fmt]
X.3.13 Formatter [big.int.fmt]
1 The specialization is enabled and constexpr-enabled ([format.formatter.spec]).
2
The member function interprets the format specification
as a
3
For the purposes of [format.string.std],
is treated as a signed integer type.
[Note:
As explained in [format.string.std],
character output is performed using .
— end note]
[big.int.literal]
X.3.14 Literals [big.int.literal]
1
Let be a character sequence obtained by concatenating
the elements of .
2
Mandates:
matches the syntax of an
3
Returns:
A object whose integer value is that
of interpreted as an
4
Remarks:
The function specialization has a non-throwing exception specification
if the effective width of the integer value returned by a function call expression
is less than or equal to .
with SBO.
If so, the specialization is .
Otherwise, each invocation of the UDL needs to allocate memory.
Our reference implementation strategy is to pre-parse a
,
the size of which can be approximated using .
[charconv]
[charconv.syn]
Change the synopsis [charconv.syn] as follows:
[charconv.to.chars]
Change [charconv.to.chars] as follows:
[…]
4
Constraints:
is a specialization of .
5
Preconditions:
has a value between and (inclusive).
6
Effects:
The integer value ([big.int.class]) of
is converted to a string of digits in the given base
(with no redundant leading zeroes).
Digits in the range 10..35 (inclusive) are represented as lowercase characters is less than zero,
the representation starts with .
7
Throws:
Nothing for functions with a parameter of type .
The overloads for may throw exceptions during allocation
unless is a power of two.
8
Remarks:
If is a power of two,
no allocation takes place.
[charconv.from.chars]
Change [charconv.from.chars] as follows:
[…]
2
Preconditions:
has a value between and (inclusive).
3
Effects:
The pattern is the expected form of the subject sequence in the locale
for the given nonzero base,
as described for , except that
no or prefix shall appear if the value of base is 2,
no or prefix shall appear if the value of base is 16, and
except that is the only sign that may appear,
and only if value has a signed type.
4
Throws:
Nothing for overloads with a parameter of type .
The overload for may throw exceptions during allocation.
5
Remarks:
If is a power of two,
at most one allocation takes place.
nor for ).
This pre-existing defect is why the Effects: element is unchanged.
[P4168R0] overhauls this specification.
[c.math.abs]
Change [c.math.abs] as follows:
1
[Note:
The headers and , , and
declare the functions described in this subclause,
but only declares the overloads for .
— end note]
2 Effects: […]
3 Remarks: […]
4
Returns:
.
5
Complexity:
Linear in the size of the representation of if is ;
otherwise constant.
6
Returns:
.
7 Complexity: Constant.
8. Acknowledgements
Thanks to Victor Zverovich for assisting with wording in § [big.int.fmt] and providing design guidance on formatting support.
Thanks to Jay Ghiron for providing extensive wording feedback.