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

We propose an infinite-precision integer in the form of a class template, with elastic operations.

Contents

1

Introduction

1.1

Infinite-precision integers in other languages

2

Motivation

2.1

Use cases

2.2

std::big_int is a vocabulary type

2.3

std::big_int for convenience and correctness

2.4

std::big_int is platform-dependent and hard to implement

2.5

std::big_int should be a compiler intrinsic for constant evaluation

3

Design

3.1

Design strategy

3.2

Layout

3.2.1

std::uint_multiprecision_t

3.2.2

C compatibility

3.2.3

Small object optimizations

3.2.3.1
SOO customization
3.2.3.2
Layout in our reference implementation
3.2.4

Sign and magnitude

3.2.4.1
Bitwise operations
3.2.5

Representation endianness

3.2.6

Access to the underlying representation

3.2.6.1
Why not provide access using a random access range?
3.2.7

Why not use reference counting?

3.2.8

Why not use tagged integers?

3.2.8.1
Problems with extendability
3.2.9

std::big_int is not a container

3.3

constexpr support

3.3.1

Circumventing transient allocations

3.4

Optimal operator overloads

3.4.1

Why not return by && from unary operators?

3.5

Why not provide lower-level operations for arithmetic?

3.6

Why no std::big_int_view?

3.6.1

Issues with std::big_int_view parameters

3.6.2

Issues with std::big_int_view allocation management

3.6.3

std::big_int_view is just not useful enough

3.7

Why no std::big_uint?

3.7.1

Burning the std::big_uint bridge

3.7.2

Conclusion

3.8

Template parameters

3.8.1

Order of template parameters

3.8.2

min_inplace_capacity

3.8.3

min_inplace_capacity restrictions

3.8.4

Limb type parameter

3.9

Conversions

3.9.1

Converting constructor

3.9.2

Explicit conversions to arithmetic types

3.9.3

Bit-precise integer interoperability

3.10

Expression templates (or lack thereof)

3.10.1

Technical reasons against expression templates

3.10.2

The problem with optional expression templates

3.10.3

Expression templates in the ISO C++ process

3.11

User-defined literals for std::big_int

3.11.1

Why 123n?

3.11.2

User-defined literals vs. string constructors

3.11.3

Why provide UDLs or string constructors at all?

3.12

Adjacent library utilities

3.12.1

Hash support for std::big_int

3.12.2

std::numeric_limits specialization

3.12.3

std::in_range

3.12.4

std::abs

3.12.5

std::gcd, std::lcm, and std::midpoint

3.12.6

std::saturating_cast

3.12.7

std::to_chars and std::from_chars

3.12.8

Formatting support

3.12.9

std::to_string and std::from_string

3.12.10

No <bit> support

3.13

Naming

3.14

Choice of header

3.15

Error handling

4

Future direction

4.1

Integer numeric functions

4.2

Fixed-point interoperability

4.3

Random number generation

4.4

Making std::uint_multiprecision_t C-compatible

4.5

Non-transient allocations

4.6

Constant template parameters

5

Arguments against standardization

5.1

Many trade-offs

5.2

Use of committee resources

5.3

Increased complexity of future integer features

5.4

Use of implementer resources

5.5

Residual fragmentation

5.5.1

N-to-N conversion problem

5.6

Inability to change ABI

5.7

Header bloat

6

Implementation experience

6.1

Deployment experience

6.2

Benchmarks

7

Wording

7.1

[headers]

7.2

[version.syn]

7.3

[numeric.limits]

7.3.1

[numeric.limits.general]

7.3.2

[numeric.special]

7.4

[utility]

7.4.1

[utility.syn]

7.4.2

[utility.intcmp]

7.5

[numeric.ops.overview]

7.6

[numeric.ops.gcd]

7.7

[numeric.ops.lcm]

7.8

[numeric.ops.midpoint]

7.9

[numeric.sat.cast]

7.10

[numeric.int.div]

7.11

[string.syn]

7.12

[string.conversions]

7.13

[big.int]

7.14

[big.int.general]

7.15

[big.int.syn]

7.16

[big.int.class]

7.16.1

[big.int.require]

7.16.2

[big.int.defns]

7.16.3

[big.int.expos]

7.16.4

[big.int.cons]

7.16.5

[big.int.ops]

7.16.6

[big.int.modifiers]

7.16.7

[big.int.conv]

7.16.8

[big.int.unary]

7.16.9

[big.int.alias]

7.16.10

[big.int.cmp]

7.16.11

[big.int.binary]

7.16.12

[big.int.hash]

7.16.13

[big.int.fmt]

7.16.14

[big.int.literal]

7.17

[charconv]

7.17.1

[charconv.syn]

7.17.2

[charconv.to.chars]

7.17.3

[charconv.from.chars]

7.18

[c.math.abs]

8

Acknowledgements

9

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 std::integer 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.

std::big_int is to long long what std::string is to char[N]: 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 int builtin
Ruby Integer builtin
Lisp integer builtin
Scheme integer builtin
Racket integer builtin
Clojure int builtin
Haskell Integer builtin
Prolog integer builtin
Wolfram Language Integer builtin
Maxima integer builtin
Erlang integer builtin
JavaScript / TypeScript bigint builtin, but not the default
Java / Kotlin BigInteger standard library
Julia BigInt standard library
C# BigInteger standard library
F# bigint standard library
Visual Basic .NET BigInteger standard library
Go big.Int standard library
Perl Math::BigInt standard library
Ada Big_Integer standard library
Zig std.math.big.int.Managed standard library
Standard ML IntInf standard library
MATLAB Symbolic Math Toolbox standard library
PHP GMP and BCMath standard library
R gmp third-party package
Swift BigInt third-party package
Rust num_bigint and crypto_bigint third-party package

Beyond that, there are also many programming languages where some third-party support is available (e.g. gmp in C or Boost.Multiprecision in C++), but there isn't as much ecosystem convergence, so these are not listed in the table.

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 llvm::APInt. Any C23 compiler also needs such a type to implement optimizations like constant folding for _BitInt(N), unless it has a small BITINT_MAXWIDTH.

Not only are infinite-precision integers available in many languages, they are also used frequently:

Language GitHub code search # Files
TypeScript language:TypeScript /\bbigint/ -is:fork 1.4M
Java language:Java /\bbigint/ -is:fork 1.3M
JavaScript language:JavaScript /\bbigint/ -is:fork 811K
C++ language:C++ /\b(big|cpp)_?u?int/ -is:fork 404K
Rust language:Rust /\bbig_?int/ -is:fork 104K

The amount of files shown here was captured at the time of writing, and is subject to change.

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 int in Python.

2. Motivation

2.1. Use cases

In terms of utility, std::big_int use cases can be classified into two buckets:

  1. 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.
  2. The use case where integers are actually big.

std::big_int 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 int), 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 std::big_int should be in the C++ standard library is that it is a vocabulary type and may appear at library boundaries.

A single code base may depend on different libraries that all interact with std::big_int:

  1. A library that loads config files (JSON, YAML, etc.) can return deserialized integers as std::big_int.
  2. Those std::big_int objects are then processed by a numeric library that uses them in e.g. statistics.
  3. The results are then stored by another library that serializes std::big_int to e.g. CSV. Alternatively, the results are stored in a relational database, using a C++ API that uses std::big_int for big integer columns in a table.

In Java, a variety of cryptographic vocabulary types are built on top of BigInteger, such as RSAPublicKey and RSAPrivateKey. 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, std::big_int 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.

Similar to Java's BigDecimal, an infinite-precision decimal floating-point type can be formed as follows:

struct big_decimal { std::big_int unscaled; int scale; // exponent for multiplication with base 10 };

The value represented here is (unscaled * pow(10, scale)).

Such a type would appear at a library boundary for communicating with a PostgreSQL database, which supports decimal and numeric 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 _Decimal128, those could not be represented. These types map onto the java.math.BigDecimal type in Java libraries.

Almost all relational databases support some kind of infinite-precision numeric type (subject to implementation limits), be that 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

In Java, such interfaces are obvious and ergonomic by comparison, operating on java.math.BigInteger or java.math.BigDecimal. 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. std::big_int is that crucial missing type.

Symbolic math libraries typically create their own 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 gmp, some using Boost.Multiprecision, etc.

std::big_int 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 std::big_int directly just like they use std::string directly.

GUI toolkits in C++ typically don't bother with direct support for big integers, even though the runtime cost of using such integers compared to rendering the GUI is negligible. For example, GTK's SpinButton takes double inputs and Qt's QSpinBox uses int.

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 123. before the user types the fractional part), input and output of a big_int or big_decimal value is clearly useful for number fields.

In some sense, std::big_int is an even more important vocabulary type than std::vector and std::string 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 char* (and maybe size_t), so while std::string is an important container type and helps with managing allocation ownership, it is arguably gratuitous as a vocabulary type. Similarly, if std::span and std::string_view 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. std::big_int could lay the foundation for passing big integers between libraries just as easily as in other languages.

Even though std::big_int is not directly C-compatible, one could pair a uint_multiprecision_t* and size_t, where the passed limb array has the same layout as std::big_int::representation. 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 std::big_int 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 std::intmax_t with boost::multiprecision::cpp_int, 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 std::big_int 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.

Some examples of platform and compiler-specific features:

In general, the intrinsics necessary to implement std::big_int 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 std::big_int is a sea of if constexpr and #ifdef 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 std::big_int is possible, an ideal implementation for constant evaluation simply delegates to the compiler's internal infinite-precision integer type (e.g. llvm::APInt 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 std::big_int.

A closely related issue is the implementation of the user-defined literal, which has the following interface in our proposal:

template<char... digits> constexpr big_int operator""n(); template<char... digits> constexpr big_int operator""N(); // now possible: big_int x = 123'456'789'012'345'678'901'234'567'890n;

The problem is that there is no other form of operator""n that enables this syntax, and the char... 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:

constexpr big_int operator""n(const uint_multiprecision_t* limbs, size_t size);

With this form,

That being said, this new form of operator""n" 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:

This culminates in the following library declarations:

// Class template: template<size_t min_inplace_capacity, class Limb = uint_multiprecision_t, class Allocator = allocator<uint_multiprecision_t>> class basic_big_int; // Alias with default in-place capacity and default allocator using big_int = basic_big_int<implementation-defined>;

Conceptually, a basic_big_int holds:

union { // In-place representation, used for small values: uint_multiprecision_t inplace_storage[N]; // Dynamic representation, used for large values: struct { uint_multiprecision_t* dynamic_storage; size_t dynamic_capacity; }; };

The underlying arithmetic is then implemented to operate on limb arrays, i.e. arrays of uint_multiprecision_t 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 std::uint_multiprecision_t, which is an unsigned integer type with the following important properties:

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 <cstdint> and <cstddef>:

3.2.2. C compatibility

While std::big_int 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 uint_multiprecision_t[] and implementing its functionality in C++. This relies on the fact that the internal layout of std::big_int is specified in detail.

One could write a header that is C-interoperable and implemented in C++:

// header.h #ifdef __cplusplus using std::uint_multiprecision_t; #else typedef /* ... */ uint_multiprecision_t; #endif #ifdef __cplusplus extern "C" #endif void do_math(const uint_multiprecision_t* data, size_t size); // source.cpp #include "header.h" extern "C" void do_math(const uint_multiprecision_t* data, size_t size) { std::big_int x{std::from_range, std::span{data, size}}; // do math with x ... }

This is analogous to having a C header taking const char* and implementing the function internally using std::string in C++.

However, this somewhat assumes that std::uint_multiprecision_t also becomes available in C, possibly in <cstdint> / <stdint.h>. 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 __UINT_MULTIPRECISION_T__ macro.

C compatibility ambitions are not in scope for the paper right now, but are worth exploring in the future.

See also §3.9.3. Bit-precise integer interoperability for discussion of interoperability with C23's _BitInt(N) (possibly coming to C++29 via [P3666R4]).

3.2.3. Small object optimizations

std::big_int (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.

By comparison, all major standard libraries have small object optimization for std::string, 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 x and y, the difference x - y, the quotient x / y, or the remainder x % y could still be fairly small numbers (possibly zero), and it is wasteful to dynamically allocate for tiny integers.

Concretely, in our benchmark results, a single-limb std::big_int 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 std::big_int is such a no-brainer and any reasonable implementation should have it, we propose to expose it to the user via the min_inplace_capacity template parameter. This parameter specifies the minimum integer width that std::basic_big_int must be able to represent without dynamic allocation. While std::big_int should be used in most cases and provides a reasonable default, there may be scenarios where

Boost.Multiprecision similarly allows customizing the SOO size by providing an argument for the MinBits template parameter of boost::multiprecision::cpp_int_backend. See also Boost documentation for cpp_int.

3.2.3.2. Layout in our reference implementation

In our [Reference-Impl], std::big_int::inplace_capacity is always 64 regardless of architecture. On 64-bit, our layout is as follows:

// Most significant bit stores the sign; // the lower 31 bits store the limb count. uint32_t size_and_sign; // The capacity of the dynamic storage in limbs. // If zero, indicates that there is no allocation and that inplace_storage is active. uint32_t capacity; union { // For values up to 64 bits. // For big_int, N = 1, but the min_inplace_capacity parameter allows more capacity // for other basic_big_int specializations. uint64_t inplace_storage[N]; // For anything that doesn't fit into inplace_storage. uint64_t* dynamic_storage; };

This layout ensures that std::big_int

For reference, OpenJDK's BigInteger implementation only supports values up to 231-1 bits (2 billion bits). V8's bigint type supports only about 1 billion bits. Some other implementations such as Rust's num_bigint crate are only limited by available memory.

3.2.4. Sign and magnitude

std::big_int 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.

Consider how to compute a & b 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

  1. negate each of the operand limbs if they belong to a negative std::big_int,
  2. perform a bitwise AND, and
  3. negate the resulting limb if the sign bit of the result is negative.

-x is equivalent to (~x + 1), 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:

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

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.

To implement an in-place &= 0xFF operation, for little-endian representation, we just need to perform a bitwise between the least significant limb and 0xFF. The limb count is always set to 1 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 std::big_int is conceptually a union of uint_multiprecision_t inplace_storage[N]; and uint_multiprecision_t* dynamic_storage;. This storage should be directly accessible using a function. Currently, we facilitate this using:

constexpr span<const uint_multiprecision_t> representation() const noexcept;

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 __int128 inplace_storage because uint_multiprecision_t cannot alias __int128.

Despite those downsides, giving the user low-level access to the storage is necessary because it enables the user to provide additional operations on std::big_int without overhead.

Consider how a user would implement their own std::popcount function (counting the number of one-bits within the integer):

int popcount(const std::big_int& x) noexcept { int sum = 0; for (std::uint_multiprecision_t d : x.representation()) { sum += std::popcount(d); } return sum; }

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 (x >> N) & M fits into a 64-bit integer and needs no allocation, any intermediate result of (x >> N) may require allocation.

Boost.Multiprecision provides access to the underlying representation via a limbs() 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 std::big_int into a corrupt state, like having a size() 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 std::span 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:

The argument of not exposing implementation details has also been used against providing access to the data of BigUint 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 BigDigit (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:

It is also important to understand that the C-compatible interface has implications even beyond std::big_int.

For example, to provide a popcount multiprecision operation (from the perspective of a C++ implementation), all sorts of code paths can converge on a single runtime library function:

long __popcount_mp(const uint_multiprecision_t*, size_t) { /* ... */ } // Because we get a std::span, we can invoke the runtime library function // in both the small and large representation case. // If we only had a random access range, // we would first need to dump the representation into a temporary array. long popcount(const std::big_int& x) { std::span<const uint_multiprecision_t> r = x.representation(); return __popcount_mp(r.data(), r.size()); } // Currently, operations like __builtin_popcount all use a software expansion // rather than calling into the runtime library, // which can lead to massive increase in code size. // __builtin_elementwise_popcount(_BitInt(8192)) in Clang 22 // literally just emits 'popcnt' 128 times and adds up the results. // // Delegating to a runtime library function would dramatically reduce code size. long popcount(const unsigned _BitInt(8192)& x) { // In the x86_64 psABI, the representation of _BitInt(8192) // is exactly the same as std::big_int with 128 limbs, // so we can just reinterpret the data. // Aliasing restrictions do not apply here. return __popcount_mp(reinterpret_cast<const uint_multiprecision_t*>(&x), sizeof(x) / sizeof(uint_multiprecision_t)); }

This would ultimately mean that popcount on std::big_int, std::popcount, and stdc_count_ones 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 std::big_int was essentially a shared_ptr<uint_multiprecision_t[]>, it would be possible to pass it cheaply by value.

A function signature as follows would not be problematic:

std::big_int abs(std::big_int x) { return x < 0 ? -std::move(x) : std::move(x); }

If x has unique ownership (i.e. the reference count is 1), -x can modify the integer in-place; otherwise, -x performs a copy.

While this idea seems clever at first, there are several problems:

By comparison, a std::big_int 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].

In the most simple form, we can use the least significant bit of an integer to indicate whether it's actually a pointer to dynamic data. Such a pointer would have greater alignment, so that bit is always zero anyway. If the least significant bit is one, the integer is a small integer

Addition would look something like:

intptr_t add_tagged(intptr_t x, intptr_t y) { if (((x & y) & 1) == 0) [[unlikely]] { // Least significant bit is zero, // so at least one of the operands is a pointer to dynamic data. // The slow path in generic_data will figure out which one has what layout. return generic_add(x, y); } intptr_t z; const bool overflow = __builtin_add_overflow(x, y, &z); // GCC builtin if (overflow) [[unlikely]] { // Neither x nor y is a pointer, but the result doesn't fit into a tagged integer, // so we call the slow path to allocate etc. return generic_add(x, y); } // Mathematically, "small" integers are (2n + 1) where n is the integer value. // We need to subtract 1 to not produce a (2n + 2) result. return (z - 1); }

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 if statement at the start, among other improvements.

Overall, such techniques provide two key benefits:

That being said, we decided not to pursue this approach for a number of reasons:

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 add_tagged optimally, ending in return (z - 1); 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.

Consider how the user would implement a popcount function ideally (which is a motivating example in §3.2.6. Access to the underlying representation):

int popcount_tagged(intptr_t x) { if ((x & 1) == 0) [[unlikely]] { return generic_popcount(x); } // Assume that for negative integers, // our popcount counts the bits in the two's complement representation. // Remember that we mathematically have (2n + 1). // // For negative integers, shifting to the left alters the bit count // since we have a bunch of leading ones. // The least significant tag bit equalizes the one lost bit. // For positive integers, we need to subtract 1 to not count the tag bit. return std::popcount(x) - (x >= 0); }

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 std::big_int API to work with.

By comparison, the proposed layout involves a uint_multiprecision_t[] directly exposed to the user,, so the std::big_int API does very little hiding and abstracting. The user could actually implement a popcount function that is not a single CPU cycle slower than a standard library implementer when only using the std::big_int public API.

3.2.9. std::big_int is not a container

When looking at the API of std::big_int, one may be tempted to think of it as a contiguous container and even to define it as such. That is, rather than having representation(), std::big_int could have begin() and end() iterators, etc.

There are a few problems with this approach:

That being said, std::big_int still has an API that is similar to a contiguous container for the purpose of familiarity. This includes functions such as shrink_to_fit(), get_allocator(), size() (in bits), reserve() (amount of bits), etc. These are mostly about allocation management, not about iterating over the representation.

3.3. constexpr support

std::big_int also provides constexpr operations, similar to std::string.

Being able to perform multiprecision arithmetic at compile time is actually part of the motivation for the paper. §2.5. std::big_int should be a compiler intrinsic for constant evaluation goes into more detail.

Without any dedicated compiler intrinsics, this means that std::big_int manages an allocation, similar to std::string. That allocation is transient, i.e. it cannot persist to runtime. However, performing operations on a std::big_int and freeing all memory before the end of the constant expression is fine.

There is another layer to the design here, which is that std::big_int 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 std::string. Because of this, we can have std::big_int values persist to runtime as long as the value is small enough to require no allocation.

// error: allocation not freed before the end of the constant expression constexpr basic_big_int<64> x = static_cast<unsigned _BitInt(1024)>(-1); // OK, -1 is representable within the object, and 64 bits are guaranteed constexpr basic_big_int<64> x = -1;

3.3.1. Circumventing transient allocations

One additional restriction on implementations to make this behavior reliable is that they must never represent a basic_big_int dynamically when inplace storage is sufficient. At runtime, implementations might hold onto an allocation until shrink_to_fit is called because that allocation may be immediately useful in the next operation.

We might do:

basic_big_int<64> x = /* ... */; // pre-existing value which holds an allocation basic_big_int<64> y = (std::move(x) - 1000) << 5;

Even if subtracting 1000 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 x 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 shrink_to_fit() 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 constexpr variable, and this would fail unnecessarily because of constexpr allocation restrictions.

If we didn't have that behavior, constexpr std::big_int 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:

Consequently, the signature of binary operators looks like:

template<class L, class R> constexpr common-big-int-type<L, R> operator+(L&& x, R&& y);

Unary operators become overload sets, like:

constexpr basic_big_int operator+() const&; constexpr basic_big_int operator+() && noexcept;

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:

x + (y * z); // operator+ reuses allocation x += y * z; // operator+= reuses allocation (x + y) / 2; // operator/ reuses allocation x - std::abs(y); // operator- reuses allocation

Since (y * z) returns basic_big_int by value, operator+ can repurpose the allocation of that result. By comparison, if operator+ worked with const&, 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 std::gcd 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:

big_int result = y * z; result += x;

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.

The hypothetical overload set for operator+ looks as follows:

constexpr basic_big_int operator+() const&; constexpr basic_big_int&& operator+() && noexcept;

It would also mean that users run danger of holding onto objects longer than they expected:

auto&& x = ~std::move(y);

If x refers to the original object y, a data race could be introduced by working with x on one thread, and overwriting y = 0; 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 std::big_int, we could (first) provide the low-level building blocks, such as multiprecision operations. Then, any third-party library could easily build its own big_int 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 span<uint_multiprecision_t>, 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:

Overall, we don't see a need for these span 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 span operations being lower-level is simply wrong, to an extent. The std::big_int level is the lowest level at which approaches such as wrapping a handle to llvm::APint is feasible. The more std::big_int is treated as a compiler intrinsic, the lower-level it conceptually becomes.

3.6. Why no std::big_int_view?

Similar to std::string_view, one might imagine a std::big_int_view (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 std::string_view as a parameter type is to accept anything string-like.

void f(std::string_view s); f("..."); // OK, callable with const char[4] f(+"..."); // OK, callable with const char* f("..."s); // OK, callable with std::string f("..."sv); // OK, callable with std::string_view

It is also easy to make f callable for additional types by providing a user-defined conversion to std::string_view.

std::big_int_view cannot provide similar functionality for integer literals. Consider that we want the call g(123) below to be valid:

struct big_int_view { const uint_multiprecision_t* data; size_t size; bool sign; big_int_view(const int& ref); // how do we implement this?! }; void g(big_int_view ref); g(123);

The key problem is turning the provided int into data and size. ref binds to a temporary object with value 123 (which is good because the lifetime of that object extends beyond the constructor call), but we cannot reinterpret that as a uint_multiprecision_t[N].

We could still turn big_int_view int a std::variant-like type that can either hold a array<uint_multiprecision_t, N> or a span<uint_multiprecision_t>, but that does not solve the problem for large integers like __int128, _BitInt(1024), etc. That requires at least an additional function:

template<size_t N> struct limb_array { array<uint_multiprecision_t, N> limbs; bool sign; }; struct big_int_view { // ... template<size_t N> big_int_view(const limb_array<N>& ref); }; auto to_limb_array(std::integral auto x) -> limb_array</* ... */>; g(to_limb_array(123)); // OK, big_int_view binds to limb_array argument, // which is a temporary object // that survives big_int_view constructor call

3.6.2. Issues with std::big_int_view allocation management

The allocation that std::big_int holds is actually very useful for intermediate operations.

A quintessential examples is that in x + y + z, since x + y is an rvalue, operator+ can reuse the allocation of x + y for the result. See also §3.4. Optimal operator overloads.

Also, thanks to std::to_chars having an rvalue overload for std::big_int, allocations can be avoided using repeated in-place division by the base. Similarly, when operator<< receives an rvalue std::big_int, it shifts the limbs in-place as if by std::shift_right if the allocation happens to be large enough. All of this happens automatically and ergonomically.

Operations on a std::big_int_view 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 std::big_int_view:

That being said, a std::big_int_view type is not entirely useless; it's just not useful enough to be included in this paper, let alone be standardized before std::big_int.

The aforementioned ability to do a non-allocating negation and std::abs can always be provided later. In the meantime, the rvalue overload of operator- provides the same functionality for mutable operands:

std::big_int x; -x; // New big_int with copied allocation and flipped sign bit. -std::move(x); // New big_int with same allocation but flipped sign bit. -std::big_int_view(x); // Possible future way of creating a "negated view".

There is some evidence of user demand for such a negated view operation. GMP has a mpz_roinit_n function for creating non-owning big integers, where the third mp_size_t xs parameter can be used to specify the sign.

3.7. Why no std::big_uint?

We do not propose an unsigned counterpart to std::big_uint, nor any way to make std::basic_big_int unsigned.

Note that it is quite rare for unsigned big integers to be provided in the first place. Boost.Multiprecision does not allow cpp_integer_type::unsigned_magnitude for arbitrary-precision cpp_int_backend.

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 std::big_uint 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.

Subtracting two BigUint in Rust's num_bigint crate panics when producing a negative value. See assert! calls in subtraction.rs.

std::big_uint 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 std::big_int and std::big_uint.

Another observation is that unsigned integers become less motivated the larger the type is. For 8-bit integers, this allows representing up to 255 instead of 127, which is a meaningful difference. When looking at §3.2.3.2. Layout in our reference implementation, making big_int 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 std::big_int 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 std::big_uint not part of this paper now, we also don't leave any room for integrating it into basic_big_int using the current set of template parameters. That is, we don't have a bool for configuring signedness. See also §3.8. Template parameters below.

A future extension is still theoretically possible. The Limb parameter could be generalized to an ABI parameter like:

struct my_unsigned_abi { using limb = std::uint_multiprecision_t; using is_unsigned = std::true_type; }; using my_big_uint = basic_big_int<big_int::inplace_capacity, my_unsigned_abi>;

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 std::big_uint in the form of std::basic_big_int extensions would be problematic. The status quo is that any code that uses std::basic_big_int 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 std::basic_big_int throw or abort on negative values would rug-pull these assumptions.

3.7.2. Conclusion

Providing std::big_uint would be in direct contradiction with our design goals, so it is not provided. The performance benefits are minimal.

Making std::basic_big_int 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 std::basic_big_uint.

3.8. Template parameters

3.8.1. Order of template parameters

For std::basic_big_int, the order is generally most likely to be specified as an argument first:

template<size_t min_inplace_capacity, class Limb = uint_multiprecision_t, class Allocator = allocator<Limb>> class basic_big_int;

The Allocator parameter needs to come after the Limb parameter so that the default argument allocator<Limb> can be spelled. This is unfortunate because the limb type cannot be configured at all (it must always be uint_multiprecision_t), but would not be so bad if it became genuinely configurable in the future. Fortunately, the only people inconvenienced are power users who use std::basic_big_int with a custom allocator, which is acceptable.

For reference, Boost.Multiprecision provides the following template parameters:

typedef unspecified-type limb_type; enum cpp_integer_type { signed_magnitude, unsigned_magnitude }; enum cpp_int_check_type { checked, unchecked }; template <unsigned MinDigits = 0, unsigned MaxDits = 0, cpp_integer_type SignType = signed_magnitude, cpp_int_check_type Checked = unchecked, class Allocator = std::allocator<limb_type> > class cpp_int_backend;

MaxDigits is used to create fixed-width integers (e.g. cpp_int_backend<128, 128> for 128-bit integers), and we consider that use case to be obsolete given [P3666R4]'s _BitInt. Checked is thus also not relevant.

3.8.2. min_inplace_capacity

Consistently with the rest of the standard library (e.g. std::array, std::bitset, std::span), the type of min_inplace_capacity is std::size_t.

The purpose of this parameter is to enable §3.2.3.1. SOO customization.

3.8.3. min_inplace_capacity restrictions

An interesting design question is whether min_inplace_capacity arguments should be disallowed if the capacity is increased internally. This might make sense because basic_big_int<7> and basic_big_int<8> 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.

Say a user needs at least 32 bits of in-place storage on a 32-bit platform. They might spell basic_big_int<32>. 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:

using my_big_int = basic_big_int<N>; static_assert(my_big_int::inplace_capacity == N);

Hypothetically, if we really insisted on imposing a restriction, we could have a basic_big_int_least alias template that would round up like basic_big_int 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:

While we are not proposing any arguments other than std::uint_multiprecision_t in this paper, such future extension could be very reasonable for a few reasons:

Even if one doesn't find these arguments compelling, is there strong enough reason to make it impossible to ever extend basic_big_int with a different limb type? Probably not. The template parameter only mildly inconveniences implementers and power users who don't merely use std::big_int. Most users work with std::string rather than std::basic_string, and we expect a similar pattern for std::big_int vs. std::basic_big_int.

A similar approach was recently taken in [P2019R9] Thread attributes, in [thread.thread.class.general]:

template <same_as<char> T> class name_hint;

Supporting 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 span<uint_multiprecision_t> could be blessed so that internally, e.g. unsigned long can alias unsigned long long and _BitInt(64). 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:

template<arbitrary-arithmetic-type T> constexpr explicit(see below) basic_big_int(T&& x) noexcept(no-alloc-constructible-from<T>);

This allows:

The no-alloc-constructible-from<T> part ensures that the constructor is noexcept if no allocation ever needs to take place to represent the value.

Consider converting std::int32_t to basic_big_int<32>. The inplace_capacity is at least 32 bits, so any int32_t can be represented without allocation, and the converting constructor is noexcept.

Since floating-point types may have non-finite values and converting those to std::big_int results in undefined behavior, conversions from floating-point types are never noexcept, consistent with the Lakos rule.

3.9.2. Explicit conversions to arithmetic types

std::big_int 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 int, but std::big_int makes this explicit. This follows the general design direction of more explicit conversions in new C++ types, like std::float64_t → std::float32_t, non-value-preserving conversions in <simd>, etc.

Besides following an established design direction, we also consider it surprising if one could convert a dynamically sized std::big_int which potentially holds thousands of bits to an int, 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 std::big_int will interoperate with _BitInt. Notably, the constructor of std::big_int 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 _BitInt from ever being a useful and fully supported type in the standard library.

One use case where _BitInt is specifically useful is for providing large integer literals. For example, std::big_int{123wb} can be used instead of 123n, which bypasses the user-defined operator""n literal, and that UDL's implementation is relatively complicated and heavy. As long as a sufficiently wide _BitInt type exists, its integer-literal is a lighter-weight alternative.

Another use case is a cryptography library that uses std::big_int at its library boundaries, but performs the underlying calculations (such as for RSA) using e.g. _BitInt(8192). This clearly requires converting std::big_int to _BitInt(8192), 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 std::big_int off to a C library that uses _BitInt(128), _BitInt(256), etc. in its API.

3.10. Expression templates (or lack thereof)

One possible direction is to provide expression templates for std::big_int operations, similar to Boost.Multiprecision (unless disabled). That is, for example, operator+ would not return a std::big_int object, but rather a std::big_int_expression<op, L, R> which could be converted to a std::big_int object when needed.

Expression templates enable two things:

Many transformations can be performed with expression templates, such as:

// Some handling of sign bits can be simplified: -(-x) → x abs(abs(x)) → abs(x) -(abs(x)) → set_sign_bit(x) // Dramatically improves performance by avoiding a huge intermediate result: pow(x, y) % m → pow_mod(x, y, m) // Better for big_int because shift constants are small and can be added cheaply: x << a << b → x << (a + b) // Avoids two passes over the data because ANDNOT is still a single binary operation // between booleans, just with an altered truth table: x & ~y → and_not(x, y) (x * y) + z → mul_add(x, y, z) popcount(x << a) → popcount(x) // ...

For fundamental integers, the compiler typically performs these optimizations automatically, but std::big_int 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:

[N4035] Implicit Evaluation of “auto” Variables and Arguments proposed a solution to the auto problem, so that even with expression templates, auto would deduce to basic_big_int. 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 number 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 std::big_int, 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.

Even if a user hated expression templates and always preferred to write pow_mod(x, y, m) explicitly, they would pay for writing pow(x, y) + z with a distinct template instantiation. That is because pow(x, y) results in a distinct type representing the expression, not in basic_big_int.

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 std::big_int 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 std::big_int, 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.

For the sake of argument, assume the aforementioned pow(x, y) % m → pow_mod(x, y, m) transformation is discovered after the fact.

By the time people's pows are being turned into pow_mods automatically, users have already been taught to write pow_mod explicitly. They might also keep using pow_mod at that point because it is more portable; the pow 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 std::big_int, we provide the following user-defined literal:

inline namespace literals { inline namespace big_int_literals { template<char... digits> constexpr big_int operator""n() noexcept(see below); template<char... digits> constexpr big_int operator""N() noexcept(see below); } }

The inline namespace approach is the same as for other standard library UDLs, such as std::literals::string_­view_literals::operator""sv.

The user could write the following code:

using namespace std::big_int_literals; auto x = 123n;

3.11.1. Why 123n?

The suffix 'n' was chosen for symmetry with JavaScript, where 123n is equivalent to BigInt(123).

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 boost::multiprecision::cpp_int("123"). We opted not to match that design because

As mentioned in §2.5. std::big_int should be a compiler intrinsic for constant evaluation, a future direction may be to extend the UDL syntax to support multiprecision, like:

constexpr big_int operator""n(const uint_multiprecision_t* limbs, size_t size);

Here, the compiler would do the work of parsing the literal and turning it into a limb array. This would avoid turning operator""n 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 123n. 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 std::big_int objects is simply too tedious.

It would also be possible using the constructor taking a span<const uint_multiprecision_t>, but this would require breaking any large constants up into individual limbs, which is very difficult to do portably considering that uint_multiprecision_t varies in size.

Another option is to rely on _BitInt, but this feature is not yet available in the C++ standard, and even if it was, the BITINT_MAXWIDTH is only guaranteed to be at least LLONG_WIDTH. In other words, _BitInts 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 std::abs and std::gcd. These are all useful for std::big_int 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 std::hash support. While the hashing algorithm is up to the implementation (there are various widely used algorithms for hashing a sequence of integers), there is an important restriction: for any basic_big_int<b, A> object, the std::hash result depends solely on the integer value (i.e. the mathematical value represented) by the object.

This makes it possible to use a different basic_big_int 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. 1 may be hashed differently than std::big_int{1}. Also note that 1 is already hashed differently from __int128, so it's not possible to have hash parity with all integer types. To maintain some degree of parity, we ensure that as available, hash<big_int>(x) equals hash<_BitInt(N)>(static_cast<_BitInt(N)>(x)) (where N is the smallest possible _BitInt width that can represent x).

3.12.2. std::numeric_limits specialization

There should be a specialization of std::numeric_limits provided for std::basic_big_int. 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 int, big_int, and boost::multiprecision::cpp_int, and distinguishing the infinite-precision integers via numeric_limits::is_bounded.

3.12.3. std::in_range

We provide std::in_range overloads for std::big_int as follows:

template<class R, size_t b, class L, class A> constexpr bool in_range(const basic_big_int<b, L, A>& t) noexcept;

While the same functionality can be achieved using t >= numeric_limits<R>::min() && t <= numeric_limits<R>::max(), std::in_range provides a more convenient and efficient way to check if a std::big_int value can be represented in another integer type.

To check if the value can be represented using a 64-bit unsigned integer, one can write:

std::in_range<std::uint64_t>(x)

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 std::big_int. The comparison operators already provide the mathematically correct result in all cases. We are also not convinced that much generic code would benefit; std::cmp_* functions are typically used when there is a known mismatch in signedness between two bounded integers.

3.12.4. std::abs

We provide std::abs overloads for std::big_int as follows:

template<size_t b, class L, class A> constexpr basic_big_int<b, L, A> abs(const basic_big_int<b, L, A>& j); template<size_t b, class L, class A> constexpr basic_big_int<b, L, A> abs(basic_big_int<b, L, A>&& j) noexcept;

This is an obviously useful overload that any user would expect to exist.

Header management provides a problem: we don't want <cstdlib> and <cmath> to include <big_int>, and both of these provide the overloads for other integer types. To solve this, <big_int> provides the overload as well as the rest of the overload set (not providing the rest of the overload set would risk ODR violations).

There is still some risk of ODR violations if std::big_int leaks through some other header, the user included only <cstdlib> and their call to std::abs with std::big_int fails, but it's unclear how this problem could arise in practice.

Both <numeric> and <charconv> properly include <big_int>, so the user gets all the associated functionality.

There is an rvalue overload because when given an rvalue of type basic_big_int, allocations can be avoided; std::abs 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 std::gcd, std::lcm, and std::midpoint overloads for std::big_int as follows:

template<class M, class N> constexpr common-big-int-type<M, N> gcd(M&& m, N&& n); template<class M, class N> constexpr common-big-int-type<M, N> lcm(M&& m, N&& n); template<class M, class N> constexpr common-big-int-type<M, N> midpoint(M&& m, N&& n);

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.

The std::midpoint 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 std::big_int as the left-hand side and e.g. _BitInt(8192) 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 std::saturating_cast overload for std::big_int as follows:

template<class R, size_t b, class L, class A> constexpr R saturating_cast(const basic_big_int<b, L, A>& x) noexcept;

… where R is a signed or unsigned integer type.

This is partially motivated by the fact that Boost's static_cast for cpp_int 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 std::big_int and fundamental integer types. In any case, providing std::saturating_cast provides an easy upgrade path from boost::multiprecision::cpp_int to std::big_int.

3.12.7. std::to_chars and std::from_chars

We also provide std::to_chars and std::from_chars overloads for std::big_int:

template<size_t b, class L, class A> constexpr to_chars_result to_chars(char* first, char* last, const basic_big_int<b, L, A>& value, int base = 10); template<size_t b, class L, class A> constexpr to_chars_result to_chars(char* first, char* last, basic_big_int<b, L, A>&& value, int base = 10); template<size_t b, class L, class A> constexpr from_chars_result from_chars(const char* first, const char* last, basic_big_int<b, L, A>& value, int base = 10);

These are clearly useful.

A notable quirk is that both std::to_chars and std::from_chars can potentially throw because they internally do arbitrary-precision arithmetic. This is also why std::to_chars has an rvalue overload; it allows reusing the allocation of value for intermediate calculations. However, std::to_chars should be guaranteed not to throw if base 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 std::to_chars is weird because it operates on a fixed-size buffer, while std::big_int is dynamically sized. However, most std::big_int 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 std::format_to to avoid an allocation.

3.12.8. Formatting support

std::big_int is also formattable out of the box, with std::format, std::print, 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. std::format("{:x}", x) prints a std::big_int x in hexadecimal.

3.12.9. std::to_string and std::from_string

We also provide std::to_string and std::to_wstring overloads for std::big_int:

template<size_t b, class L, class A> constexpr string to_string(const basic_big_int<b, L, A>& val); template<size_t b, class L, class A> constexpr string to_string(basic_big_int<b, L, A>&& val); template<size_t b, class L, class A> constexpr wstring to_wstring(const basic_big_int<b, L, A>& val); template<size_t b, class L, class A> constexpr wstring to_wstring(basic_big_int<b, L, A>&& val);

Since these are defined in terms of std::format with a {} specifier, the specification is trivial and the same as for any fundamental integer type. In practice, an implementation would directly call std::to_chars to cut out the middle-man between std::to_string and std::to_chars.

std::to_chars has an rvalue overload so that the allocation of val can be reused for intermediate calculations. std::to_string and std::to_wstring also need such an overload to avoid pessimization.

3.12.10. No <bit> support

While bit-manipulation functions are useful for std::big_int, 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:

3.13. Naming

The name std::big_int 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 std::big_int is also immediately recognizable as supporting infinite-precision arithmetic and as growing elastically as needed.

Previous proposals used the name std::integer, but this name doesn't convey the design well, and is too similar to concept names like std::integral.

3.14. Choice of header

We propose a new <big_int> header that just provides std::big_int and its utility functions.

While std::big_int could belong in <numeric>, that header is already enormous, and std::big_int is essentially a standalone container. It could be seen as a std::vector with some operator overloads. Containers traditionally have their own headers, like <vector>.

3.15. Error handling

std::big_int has elastic operations, meaning that it grows as needed to fit the result, making many operations infallible. The allocations can still fail and throw std::bad_alloc, same as for any other container.

Fallible operations include

We handle this as undefined behavior. From a runtime cost perspective, this is unjustified because operator/ 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. std::big_int 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. std::bitset offers some precedent, but arguably goes too far by throwing on runtime checks even in fast and low-level operations like operator[], 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 std::big_int 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 std::big_int overloads for pre-existing functions, such as std::abs. However, infinite-precision libraries typically provide many more operations, such as

The key observation is that these functions are at least as useful for fundamental integer types, not just for std::big_int, and they should be designed for integers in general. That makes all of them separate and orthogonal features, where a std::big_int overload is just one of the design aspects.

The bigint type in JavaScript was standardized in such a minimal form as well: only the numeric type itself is provided, while the additional Math 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

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 std::countl_zero overload for std::big_int 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 std::big_int in <random> 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 std::big_int in std::uniform_int_distribution 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 std::uint_multiprecision_t in <cstdint>, and possibly by standardizing the type for C2y.

4.5. Non-transient allocations

std::big_int would benefit from non-transient constexpr allocations because it would allow constexpr variables to hold any integer value. Currently, the limit is imposed by the inplace_capacity.

However, this is a general problem, and any solution should include std::string, std::vector, etc. Recent proposals dealing with this problem include:

4.6. Constant template parameters

While std::big_int'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 std::big_int becomes usable as a template parameter. That is, 123n could be mangled as W123 similar to how 123 is mangled as i123.

However, again, this is is a general problem, and other types such as std::string 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 std::big_int in the standard, there are also some arguments against doing so, which need to be considered carefully.

5.1. Many trade-offs

std::big_int 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 std::big_int (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.
  • Providing a single std::big_int non-template alias prevents a pessimization of the interface where the user would always have to provide the small_capacity and where there would be no true vocabulary type.
  • In the use case where std::big_int always stores huge values, the cost of allocation and arithmetic dominates the small added cost of increased container size and branching.
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 std::basic_big_int<128>.
Allocation Different use cases require different forms of allocation. We address this by making std::big_int allocator-aware.
Constant-time operations In certain applications such as in cryptography, constant-time operations are desired, and std::big_int is not designed around constant execution time.
  • Constant-time execution requires constant integer sizes, so this use case is actually covered by _BitInt, not std::big_int. It's fundamentally implausible for 1-bit integer operations to have the same cost as 1024-bit integer operations, no matter what design trade-offs are made. In other words, there is no trade-off because std::big_int would not be used for this purpose anyway.
  • std::big_int is still useful as a vocabulary type at cryptographic library boundaries. For example, Java's BouncyCastle cryptographic library uses BigInteger in its API, but the underlying cryptographic algorithms may operate on fixed-width arrays of integers. See also §2.2. std::big_int is a vocabulary type.
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 llvm::APInt are a rare exception.
Elasticity std::big_int operations are elastic (meaning growth happens automatically to fit the result), but some use cases require inelastic results, like llvm::APInt. This is arguably not a trade-off because inelastic operations can always be added later, via e.g. std::fixed_add or std::wrapping_add functions. The trade-off is limited to the fact that those operations would not simply be spelled operator+.
Error handling std::big_int is entirely non-throwing, except for exceptions thrown during allocation failure or as the result of undefined behavior (e.g. division by zero in operator/).
  • Making the type mostly non-throwing is the most flexible choice and even caters to freestanding use cases. Due to operations being elastic, almost none of them require error handling anyway, except for allocation, the error handling of which can be customized.
  • The undefined behavior in the rare error cases does not preclude anyone from using the type. They can always add their own runtime checks where needed to prevent UB. It would be much less flexible if the runtime check was forced and came with a certain behavior, like throwing an exception.

In summary, the idea that std::big_int has too many domain-specific trade-offs is largely unsubstantiated. Allowing configuration of the inplace_capacity and allocator provides sufficient flexibility for virtually every use case. Any remaining trade-off in the design of std::big_int would be mildly annoying at worst.

That does not mean that no one will ever have to make their own std::big_int type; there will always be people creating their own std::vector, their own std::sort, etc. because their use is simply too specialized for the standard library. Some people even make their own standard library. However, our std::big_int 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 std::big_int 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 std::big_int, like not being optimized with manual assembly for some architecture used in scientific computing, but std::big_int 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 std::big_int 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 std::big_int is to closely imitate the fundamental integer types. Rather than providing operations as member functions, existing standard library facilities like std::gcd and std::popcount should be extended to support std::big_int in addition to signed and/or unsigned integer types. Consequently, once a decent amount of coverage for std::big_int is provided, the expectation is that any new integer utility would also support std::big_int. This is very similar to how any new functions in <cmath> or <bit> are now expected to have std::simd overloads, for the sake of consistency.

We don't consider this permanent increase in standardization effort to detract from std::big_int because

In short, the added cost to new integer utilities is either low or effectively zero (if we would have added std::big_int support anyway, it's not really an additional cost).

5.4. Use of implementer resources

Another similar criticism is that std::big_int 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:

The biggest remaining concern is that standardization trifurcates the implementations into a libstdc++ std::big_int, a libc++ std::big_int, and an MSVC std::big_int. 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 std::big_int 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 std::big_int 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 std::string_view does not mean that everyone would instantly move away from existing solutions like gsl::string_view, 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 boost::multiprecision::cpp_int to e.g. OpenSSL's BIGNUM? 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.

std::big_int 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 const std::big_int& or std::span<const std::uint_multiprecision_t> 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 std::big_int 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 std::big_int 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:

In conclusion, while the ABI freeze is a real concern with std::big_int, 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 std::big_int is to provide supporting utilities as overloads of existing standard library functions. That is, std::to_chars, std::gcd, etc. receive new overloads accepting std::big_int. An unfortunate consequence is that this bloats any and all integer utilities with std::big_int support, including formerly lightweight headers like <charconv>.

To be fair:

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, std::big_int is heavily modeled after Boost.Multiprecision and boost::multiprecision::cpp_int 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 std::big_int implementation is typically a bit slower than gmp_int (Boost wrapper around GMP) and on par with boost::multiprecision::cpp_int 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:

<big_int>

[version.syn]

Add a feature-test macro to [version.syn] as follows:

#define __cpp_lib_big_int 20XXXXL // freestanding, also in <big_int>

[numeric.limits]

[numeric.limits.general]

Change [numeric.limits.general] paragraph 5 as follows:

Non-arithmetic standard types, such as complex<T> ([complex]), shall not have specializations, except for specializations of basic_big_int ([big.int]).

[numeric.special]

Insert a new paragraph at the end of [numeric.special] as follows:

The specialization for basic_big_int ([big.int]) shall be provided as follows:

namespace std { template<size_t b, class L, class A> class numeric_limits<basic_big_int<b, L, A>> { private: using value-type = basic_big_int<b, L, A>; public: static constexpr bool is_specialized = true; static constexpr value-type min() noexcept { return 0; } static constexpr value-type max() noexcept { return 0; } static constexpr value-type lowest() noexcept { return 0; } static constexpr int digits = 0; static constexpr int digits10 = 0; static constexpr int max_digits10 = 0; static constexpr bool is_signed = true; static constexpr bool is_integer = true; static constexpr bool is_exact = true; static constexpr int radix = 2; static constexpr value-type epsilon() noexcept { return 0; } static constexpr value-type round_error() noexcept { return 0; } static constexpr int min_exponent = 0; static constexpr int min_exponent10 = 0; static constexpr int max_exponent = 0; static constexpr int max_exponent10 = 0; static constexpr bool has_infinity = false; static constexpr bool has_quiet_NaN = false; static constexpr bool has_signaling_NaN = false; static constexpr value-type infinity() noexcept { return 0; } static constexpr value-type quiet_NaN() noexcept { return 0; } static constexpr value-type signaling_NaN() noexcept { return 0; } static constexpr value-type denorm_min() noexcept { return 0; } static constexpr bool is_iec559 = false; static constexpr bool is_bounded = false; static constexpr bool is_modulo = false; static constexpr bool traps = false; static constexpr bool tinyness_before = false; static constexpr float_round_style round_style = round_toward_zero; }; }

[utility]

[utility.syn]

Change the synopsis [utility.syn] as follows:

// all freestanding #include <compare> // see [compare.syn] #include <initializer_list> // see [initializer.list.syn] #include <big_int> // see [big.int] namespace std { […] template<class R, class T> constexpr bool in_range(T t) noexcept; template<class R, size_t b, class L, class A> constexpr bool in_range(const basic_big_int<b, L, A>& t) noexcept; […] }

[utility.intcmp]

Change [utility.intcmp] as follows:

[…]

template<class R, class T> constexpr bool in_range(T t) noexcept;

8 Mandates: Each of T and R is a signed or unsigned integer type ([basic.fundamental]).

9 Effects: Equivalent to:

return cmp_greater_equal(t, numeric_limits<R>::min()) && cmp_less_equal(t, numeric_limits<R>::max());
template<class R, size_t b, class L, class A> constexpr bool in_range(const basic_big_int<b, L, A>& t) noexcept;

10 Mandates: R is a signed or unsigned integer type ([basic.fundamental]).

11 Returns: t >= numeric_limits<R>::min() && t <= numeric_limits<R>::max().

12 Complexity: Constant.

The seemingly magical constant complexity can be achieved by checking the sign bit and size(). std::in_range 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 R.

See also §3.12.3. std::in_range.

[numeric.ops.overview]

Change the synopsis [numeric.ops.overview] as follows:

#include <big_int> // see [big.int] // mostly freestanding namespace std { […] // [numeric.ops.gcd], greatest common divisor template<class M, class N> constexpr common_type_t<M, N> gcd(M m, N n); template<class M, class N> constexpr common-big-int-type<M, N> gcd(M&& m, N&& n); // [numeric.ops.lcm], least common multiple template<class M, class N> constexpr common_type_t<M, N> lcm(M m, N n); template<class M, class N> constexpr common-big-int-type<M, N> lcm(M&& m, N&& n); // [numeric.ops.midpoint], midpoint template<class T> constexpr T midpoint(T a, T b) noexcept; template<class T> constexpr T* midpoint(T* a, T* b); template<class M, class N> constexpr common-big-int-type<M, N> midpoint(M&& a, N&& b); […] template<class T, class U> constexpr T saturating_cast(U x) noexcept; template<class R, size_t b, class L, class A> constexpr R saturating_cast(const basic_big_int<b, L, A>& x) noexcept; }

[numeric.ops.gcd]

Append a new item to [numeric.ops.gcd] as follows:

template<class M, class N> constexpr common-big-int-type<M, N> gcd(M&& m, N&& n);

Effects: Equivalent to:

return big-int-combine([](auto m_int, auto n_int) { return gcd(m_int, n_int); }, std::forward<M>(m), std::forward<N>(n));

Complexity: O( nm ) , where n and m are the representation sizes of a and b, respectively.

See also §3.12.5. std::gcd, std::lcm, and std::midpoint.

[numeric.ops.lcm]

Append a new item to [numeric.ops.lcm] as follows:

template<class M, class N> constexpr common-big-int-type<M, N> lcm(M&& m, N&& n);

Effects: Equivalent to:

return big-int-combine([](auto m_int, auto n_int) { return lcm(m_int, n_int); }, std::forward<M>(m), std::forward<N>(n));

Complexity: O( nm ) , where n and m are the representation sizes of a and b, respectively.

See also §3.12.5. std::gcd, std::lcm, and std::midpoint.

[numeric.ops.midpoint]

Append a new item to [numeric.ops.midpoint] as follows:

template<class A, class B> constexpr common-big-int-type<A, B> midpoint(A&& a, B&& b);

Effects: Equivalent to:

return big-int-combine([](auto a_int, auto b_int) { return midpoint(a_int, b_int); }, std::forward<A>(a), std::forward<B>(b));

Complexity: O( nm ) , where n and m are the representation sizes of a and b, respectively.

See also §3.12.5. std::gcd, std::lcm, and std::midpoint.

[numeric.sat.cast]

Change [numeric.sat.cast] as follows:

template<class R, class T> constexpr R saturating_cast(T x) noexcept;

1 Constraints: R and T are signed or unsigned integer types ([basic.fundamental]).

2 Returns: If x is representable as a value of type R, x; otherwise, either the largest or smallest representable value of type R, whichever is closer to the value of x.

template<class R, size_t b, class L, class A> constexpr R saturating_cast(const basic_big_int<b, L, A>& x) noexcept;

3 Constraints: R is a signed or unsigned integer type ([basic.fundamental]).

4 Returns: If the integer value ([big.int.class]) v of x is representable as a value of type R, v; otherwise, either the largest or smallest representable value of type R, whichever is closer to v.

5 Complexity: Constant.

See also §3.12.6. std::saturating_cast.

[numeric.int.div]

This is just a rough sketch of how to go about computing the quotient and remainder at the same time, which is extremely important to avoid overhead from performing division twice.

There is a proposal [P3724R3] in the pipeline which adds the initial set of functions.

template<class T> constexpr div_result<T> div_rem_to_zero(T x, T y); template<class L, class R> constexpr div_result<common-big-int-type<L, R>> div_rem_to_zero(L&& x, R&& y);

[string.syn]

Change [string.syn] as follows:

#include <big_int> // see [big.int] #include <compare> // see [compare.syn] #include <initializer_list> // see [initializer.list.syn] namespace std { […] // [string.conversions], string conversions […] constexpr string to_string(int val); constexpr string to_string(unsigned val); constexpr string to_string(long val); constexpr string to_string(unsigned long val); constexpr string to_string(long long val); constexpr string to_string(unsigned long long val); string to_string(float val); string to_string(double val); string to_string(long double val); template<size_t b, class L, class A> constexpr string to_string(const basic_big_int<b, L, A>& val); template<size_t b, class L, class A> constexpr string to_string(basic_big_int<b, L, A>&& val); […] constexpr wstring to_wstring(int val); constexpr wstring to_wstring(unsigned val); constexpr wstring to_wstring(long val); constexpr wstring to_wstring(unsigned long val); constexpr wstring to_wstring(long long val); constexpr wstring to_wstring(unsigned long long val); wstring to_wstring(float val); wstring to_wstring(double val); wstring to_wstring(long double val); template<size_t b, class L, class A> constexpr wstring to_wstring(const basic_big_int<b, L, A>& val); template<size_t b, class L, class A> constexpr wstring to_wstring(basic_big_int<b, L, A>&& val); […] }

[string.conversions]

Change [string.conversions] as follows:

[…]

constexpr string to_string(int val); constexpr string to_string(unsigned val); constexpr string to_string(long val); constexpr string to_string(unsigned long val); constexpr string to_string(long long val); constexpr string to_string(unsigned long long val); string to_string(float val); string to_string(double val); string to_string(long double val);

Returns: format("{}", val).

template<size_t b, class L, class A> constexpr string to_string(const basic_big_int<b, L, A>& val);

Returns: format("{}", val).

template<size_t b, class L, class A> constexpr string to_string(basic_big_int<b, L, A>&& val);

Returns: format("{}", val).

Postconditions: val is in a valid but unspecified state.

The intent is to allow the implementation to call the rvalue overload of std::to_chars. Simply passing std::move(val) into std::format wouldn't do anything because formatting does not handle rvalues specially.

It would also be possible to specify to_string in terms of to_chars 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.

[…]

constexpr wstring to_wstring(int val); constexpr wstring to_wstring(unsigned val); constexpr wstring to_wstring(long val); constexpr wstring to_wstring(unsigned long val); constexpr wstring to_wstring(long long val); constexpr wstring to_wstring(unsigned long long val); wstring to_wstring(float val); wstring to_wstring(double val); wstring to_wstring(long double val);

Returns: format(L"{}", val).

template<size_t b, class L, class A> constexpr wstring to_wstring(const basic_big_int<b, L, A>& val);

Returns: format(L"{}", val).

template<size_t b, class L, class A> constexpr wstring to_wstring(basic_big_int<b, L, A>&& val);

Returns: format(L"{}", val).

Postconditions: val 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 <big_int> 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 uint_multiprecision_t objects.

Unfortunately, operations on uint_multiprecision_t 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. operator==, 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]

#include <compare> #include <span> // mostly freestanding namespace std { // alias uint_multiprecision_t using uint_multiprecision_t = see below; // [big.int.class], class template basic_big_int template<size_t min_inplace_capacity, class Limb = uint_multiprecision_t, class Allocator = allocator<Limb>> class basic_big_int; // [big.int.expos], exposition-only helpers template<class T> concept signed-or-unsigned = see below; // exposition only template<class T> concept arbitrary-integer = see below; // exposition only template<class T> concept arbitrary-arithmetic-type = see below; // exposition only template<class L, class R> using common-big-int-type = see below; // exposition only template<class T, class U> concept common-big-int-type-with = requires { // exposition only typename common-big-int-type<T, U>; }; template<class F, class L, class R> constexpr common-big-int-type<L, R> big-int-combine(F f, L&& x, R&& y); // [big.int.alias], alias big_int using big_int = basic_big_int<see below>; // [big.int.cmp], non-member comparison operator functions template<class L, common-big-int-type-with<L> R> constexpr bool operator==(const L& lhs, const R& rhs) noexcept; template<class L, common-big-int-type-with<L> R> constexpr strong_ordering operator<=>(const L& lhs, const R& rhs) noexcept; // [big.int.binary], binary operations template<class L, class R> constexpr common-big-int-type<L, R> operator+(L&& x, R&& y); template<class L, class R> constexpr common-big-int-type<L, R> operator-(L&& x, R&& y); template<class L, class R> constexpr common-big-int-type<L, R> operator*(L&& x, R&& y); template<class L, class R> constexpr common-big-int-type<L, R> operator/(L&& x, R&& y); template<class L, class R> constexpr common-big-int-type<L, R> operator%(L&& x, R&& y); template<class L, class R> constexpr common-big-int-type<L, R> operator&(L&& x, R&& y); template<class L, class R> constexpr common-big-int-type<L, R> operator|(L&& x, R&& y); template<class L, class R> constexpr common-big-int-type<L, R> operator^(L&& x, R&& y); template<class T, signed-or-unsigned S> constexpr remove_cvref_t<T> operator<<(T&& x, S s); template<class T, signed-or-unsigned S> constexpr remove_cvref_t<T> operator>>(T&& x, S s); namespace pmr { template<size_t b, class L> using basic_big_int = std::basic_big_int<b, L, polymorphic_allocator<L>>; using big_int = basic_big_int<std::big_int::inplace_bits>; } // swap template<size_t b, class L, class A> void swap(basic_big_int<b, L, A>& x, basic_big_int<b, L, A>& y) noexcept(noexcept(x.swap(y))) { x.swap(y); } // [big.int.hash], hash support template<class T> struct hash; template<size_t b, class L, class A> struct hash<basic_big_int<b, L, A>>; // [big.int.fmt], formatter template<size_t b, class L, class A, class charT> struct formatter<basic_big_int<b, L, A>, charT>; // [big.int.literal], literals inline namespace literals { inline namespace big_int_literals { template<char... digits> constexpr big_int operator""n() noexcept(see below); template<char... digits> constexpr big_int operator""N() noexcept(see below); } } // [c.math.abs], absolute values constexpr int abs(int j); constexpr long int abs(long int j); constexpr long long int abs(long long int j); constexpr floating-point-type abs(floating-point-type j); // freestanding-deleted template<size_t b, class L, class A> constexpr basic_big_int<b, L, A> abs(const basic_big_int<b, L, A>& j); template<size_t b, class L, class A> constexpr basic_big_int<b, L, A> abs(basic_big_int<b, L, A>&& j) noexcept; }

1 The type alias uint_multiprecision_t denotes a standard unsigned or extended unsigned integer type ([basic.fundamental]) which has no padding bits.

2 Recommended practice: uint_multiprecision_t 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.

See also §3.2.1. std::uint_multiprecision_t.

[big.int.class]

X.3 Class template basic_big_int [big.int.class]

1 The class template basic_big_int 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.

template<size_t min_inplace_capacity, class Limb = uint_multiprecision_t, class Allocator = allocator<Limb>> class basic_big_int { // [big.int.defns], types and constants using allocator_type = Allocator; using size_type = implementation-defined; static constexpr size_type inplace_representation_capacity = see below; static constexpr size_type inplace_capacity = see below; // [big.int.expos], exposition-only helpers template<class T> inline constexpr bool no-alloc-constructible-from = see below; // exposition only // [big.int.cons], construct/copy/destroy constexpr basic_big_int() noexcept(noexcept(Allocator())); constexpr explicit basic_big_int(const Allocator& a) noexcept; constexpr basic_big_int(const basic_big_int& x); constexpr basic_big_int(basic_big_int&& x) noexcept; constexpr basic_big_int(const basic_big_int& x, const type_identity_t<Allocator>& a); constexpr basic_big_int(basic_big_int&& x, const type_identity_t<Allocator>& a); template<arbitrary-arithmetic-type T> constexpr explicit(see below) basic_big_int(T&& x) noexcept(no-alloc-constructible-from<T>); template<arbitrary-arithmetic-type T> constexpr explicit basic_big_int(const T& x, const Allocator& a) noexcept(no-alloc-constructible-from<T>); template <input_iterator I, sentinel_for<I> S> requires signed-or-unsigned<iter_value_t<I>> constexpr basic_big_int(I begin, S end, const Allocator& a = Allocator()); template<input_range R> requires signed-or-unsigned<ranges::range_value_t<R>> constexpr basic_big_int(from_range_t, R&&, const Allocator& a = Allocator()); constexpr ~basic_big_int(); // [big.int.ops], operations constexpr span<const uint_multiprecision_t> representation() const noexcept; constexpr size_type size() const noexcept; constexpr size_type representation_size() const noexcept; constexpr size_type max_size() const noexcept; constexpr size_type max_representation_size() const noexcept; constexpr size_type capacity() const noexcept; constexpr size_type representation_capacity() const noexcept; constexpr allocator_type get_allocator() const noexcept; constexpr void reserve(size_type n); constexpr void reserve_representation(size_type n); constexpr void shrink_to_fit(); // [big.int.modifiers], modifiers constexpr basic_big_int& operator=(const basic_big_int& x); constexpr basic_big_int& operator=(basic_big_int&& x) noexcept; template<arbitrary-integer T> constexpr basic_big_int& operator=(T&& x) noexcept(no-alloc-constructible-from<T>); template<common-big-int-type-with<basic_big_int> T> constexpr basic_big_int& operator+=(T&& x); template<common-big-int-type-with<basic_big_int> T> constexpr basic_big_int& operator-=(T&& x); template<common-big-int-type-with<basic_big_int> T> constexpr basic_big_int& operator*=(T&& x); template<common-big-int-type-with<basic_big_int> T> constexpr basic_big_int& operator/=(T&& x); template<common-big-int-type-with<basic_big_int> T> constexpr basic_big_int& operator%=(T&& x); template<common-big-int-type-with<basic_big_int> T> constexpr basic_big_int& operator&=(T&& x); template<common-big-int-type-with<basic_big_int> T> constexpr basic_big_int& operator|=(T&& x); template<common-big-int-type-with<basic_big_int> T> constexpr basic_big_int& operator^=(T&& x); template<signed-or-unsigned S> constexpr basic_big_int& operator<<=(S s); template<signed-or-unsigned S> constexpr basic_big_int& operator>>=(S s); constexpr void swap(basic_big_int& x) noexcept(allocator_traits<Allocator>::propagate_on_container_swap::value || allocator_traits<Allocator>::is_always_equal::value); // [big.int.conv], conversions template<class T> constexpr explicit operator T() const noexcept; // [big.int.unary], unary operations constexpr basic_big_int operator+() const&; constexpr basic_big_int operator+() && noexcept; constexpr basic_big_int operator-() const&; constexpr basic_big_int operator-() && noexcept; constexpr basic_big_int operator~() const&; constexpr basic_big_int operator~() &&; constexpr basic_big_int& operator++(); basic_big_int operator++(int) = default; constexpr basic_big_int& operator--(); basic_big_int operator--(int) = default; };

2 The program is ill-formed if template parameter Limb is not uint_multiprecision_t.

3 A basic_big_int 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 basic_big_int is either represented using subobjects nested within a basic_big_int or represented within storage obtained from the given Allocator; the sign of the integer value is represented separately.

4 The effective width of an integer value x is the width of the smallest hypothetical unsigned integer type ([basic.fundamental]) able to represent the magnitude of x.

5 Template parameter min_inplace_capacity specifies the minimum width of integers that a basic_big_int can represent using a subobject nested within. min_inplace_capacity 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]).

This is intended to apply to _BitInt as well. That is, std::big_int is able to represent any _BitInt value without allocations, if the user specifies a sufficiently large min_inplace_capacity. In practice, this is trivial to implement because min_inplace_capacity simply specifies the size of the uint_multiprecision_t[] array inside basic_big_int.

[big.int.require]

X.3.1 General requirements [big.int.require]

1 If any operation would cause size() to exceed max_size(), that operation throws an exception object of type length_error.

2 If any member function or operator of basic_big_int throws an exception, that function or operator has no other effect on the basic_big_int object.

3 Every object of type basic_big_int<min_inplace_capacity, Limb, Allocator uses an object of type Allocator to allocate and free storage for the contained uint_multiprecision_t objects as needed. Allocator shall meet the Cpp17Allocator requirements ([allocator.requirements.general]).

This wording up to this point is copied almost verbatim from [string.require].

4 The representation of a basic_big_int object is the sequence of uint_multiprecision_t elements that collectively represents the integer value of that object. Unless otherwise stated,

described in [big.int] invalidates the representation of the object, meaning that results previously returned by representation() are no longer valid. Whenever a basic_big_int object is left in a valid but unspecified state, its representation is considered invalidated.

5 Any function that has a basic_big_int&& parameter and any member function of basic_big_int with a && ref-qualifier leaves the corresponding argument in a valid but unspecified state.

6 During constant evaluation, if the effective width of the integer value of a basic_big_int is less than or equal to its inplace_bits member, the object holds no allocation following any operation.
[Note: The behavior is as if shrink_to_fit() were called following every operation that may allocate. — end note]

See §3.3.1. Circumventing transient allocations.

[big.int.defns]

X.3.2 Types and constants [big.int.defns]

static constexpr size_type inplace_representation_capacity = see below;

1 The value of the static data member inplace_representation_capacity is the amount of uint_multiprecision_t nested within a basic_big_int object and which participates in representing its integer value.

2 Remarks: The value of inplace_representation_capacity shall be at least div_to_pos_inf(min_inplace_capacity, numeric_limits<uint_multiprecision_t>::digits).

static constexpr size_type inplace_capacity = inplace_representation_capacity * numeric_limits<uint_multiprecision_t>::digits;

3 Remarks: The instantiation is ill-formed if the multiplication is not the mathematical product of the two factors.

The last remark deals with the edge case where inplace_representation_capacity is representable in size_type, but the multiplication leads to overflow/wrapping. It effectively imposes a limit on the user-provided min_inplace_capacity.

[big.int.expos]

X.3.3 Exposition-only helpers [big.int.expos]

template<class T> concept signed-or-unsigned = see below;

1 The exposition-only concept signed-or-unsigned is satisfied and modeled if and only if T is a signed or unsigned integer type ([basic.fundamental]).

template<class T> concept arbitrary-integer = see below;

2 The exposition-only concept arbitrary-integer is satisfied and modeled if and only if remove_cvref_t<T> is either a signed or unsigned integer type ([basic.fundamental]) or a specialization of basic_big_int.

template<class T> concept arbitrary-arithmetic-type = see below;

3 The exposition-only concept arbitrary-arithmetic-type is satisfied and modeled if and only if remove_cvref_t<T> is either a cv-unqualified arithmetic type ([basic.fundamental]) or a specialization of basic_big_int.

template<class L, class R> using common-big-int-type = see below;

4 Let LT be remove_cvref_t<L>, and let RT be remove_cvref_t<R>.

5 Result:

  • If LT and RT are the same specialization of basic_big_int, LT;
  • otherwise, if LT is a specialization of basic_big_int and RT is a signed or unsigned integer type ([basic.fundamental]), LT;
  • otherwise, if RT is a specialization of basic_big_int and LT is a signed or unsigned integer type ([basic.fundamental]), RT;
  • otherwise, the type alias is ill-formed.
template<class T> inline constexpr bool no-alloc-constructible-from = see below;

6 Effects: no-alloc-constructible-from is true if remove_cvref_t<T> is a signed or unsigned integer type whose width is less than or equal to inplace_bits, and false otherwise.

template<class L, class R, class F> constexpr common-big-int-type<L, R> big-int-combine(F f, L&& x, R&& y);

7 Let:

  • LT be remove_cvref_t<L>.
  • RT be remove_cvref_t<R>.
  • T be a hypothetical signed integer type with sufficient range to represent the integer values of x, y, and of f(static_cast<T>(x), static_cast<T>(y)).
  • p be x or y, chosen as follows:
    • If exactly one of LT or RT is a specialization of basic_big_int, p is x or y, respectively.
    • Otherwise, if exactly one of L or R is not an lvalue reference, p is x or y, respectively.

      This covers the case where there is exactly one given basic_big_int rvalue, so its allocation should be reused rather than unnecessarily copying.

    • Otherwise, it is a property of the implementation whether p is x or 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.

8 Returns: A basic_big_int object whose integer value is that of f(static_cast<T>(x), static_cast<T>(y)) and whose allocator is obtained from p.

9 Remarks: If L is a specialization of basic_big_int, x is left in an unspecified but valid state. If R is a specialization of basic_big_int, y is left in an unspecified but valid state. If p is not left in an unspecified state, the allocator of the result object is initialized from allocator_traits<Allocator>::select_on_container_copy_construction(p.get_allocator()).
[Note: Both operands can be left in an unspecified state. — end note]

The big-int-combine utility is needed for all sorts of operations, such as binary operators and std::gcd and std::lcm.

The handling of allocators is similar to operator+ between basic_strings ([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 select_on_container_copy_construction 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 std::move like in the move constructor, but may also be copied.

[big.int.cons]

X.3.4 Construct/copy/destroy [big.int.cons]

constexpr basic_big_int() noexcept;

1 Effects: Initializes the integer value to zero. Value-initializes the allocator.

2 Complexity: Constant.

constexpr explicit basic_big_int(const Allocator& a) noexcept;

3 Effects: Initializes the integer value to zero. Initializes the allocator to a.

4 Complexity: Constant.

constexpr basic_big_int(const basic_big_int& x);

5 Effects: Initializes the integer value to that of x. Initializes the allocator to allocator_traits<Allocator>::select_on_container_copy_construction(x.get_allocator()).

The use of select_on_container_copy_construction mirrors container requirements imposed in [container.requirements].

6 Throws: Nothing if the effective width of the integer value of x is less than or equal to inplace_bits; otherwise, exceptions thrown during allocation.

7 Complexity: Linear in the size of the representation of x.

constexpr basic_big_int(basic_big_int&& x) noexcept;

8 Effects: Initializes the integer value to that of x. Initializes the allocator to std::move(x.get_allocator()).

The use of std::move(x.get_allocator()) mirrors container requirements imposed in [container.requirements].

9 Complexity: Constant.

constexpr basic_big_int(const basic_big_int& x, const type_identity_t<Allocator>& a); constexpr basic_big_int(basic_big_int&& x, const type_identity_t<Allocator>& a);

10 Effects: Initializes the integer value to that of x. Initializes the allocator to a.

11 Complexity: Linear in the size of the representation of x.

template<arbitrary-arithmetic-type T> constexpr explicit(see below) basic_big_int(T&& x) noexcept(no-alloc-constructible-from<T>);

12 Constraints: is_same_v<basic_big_int, remove_cvref_t<T>> is false.

This constraint ensures that there is no ambiguity with the copy constructor or move constructor. Also note that we support construction from basic_big_int with other allocators or with the same allocator but different min_inplace_capacity.

13 Preconditions: If remove_cvref_t<T> is a floating-point type, the value of x is finite.

14 Effects: If remove_cvref_t<T> is an integral type or a specialization of basic_big_int, initializes the integer value to that of x. Otherwise, remove_cvref_t<T> is a floating-point type, and this object is initialized to the integer value obtained by discarding the fractional part of x.

15 Throws: Nothing if the effective width of the integer value this object is initialized with is less than or equal to inplace_bits; otherwise, exceptions thrown during allocation.

16 Remarks: The constructor is explicit if remove_cvref_t<T> is neither a signed or unsigned integer type ([basic.fundamental]) nor the current specialization of basic_big_int.

The design goal here is to permit conversion from any arithmetic type as well as for basic_big_int specializations with other allocators, but to make allocator mixing and floating-point conversions explicit. Also explicit is the conversion from character types to basic_big_int, which is arguably needed because character types and integers are used in different domains.

template<arbitrary-arithmetic-type T> constexpr basic_big_int(const T& x, const Allocator& a) noexcept(no-alloc-constructible-from<T>);

17 Preconditions: If remove_cvref_t<T> is a floating-point type, the value of x is finite.

18 Effects: If remove_cvref_t<T> is an integral type or a specialization of basic_big_int, initializes the integer value to that of x. Otherwise, remove_cvref_t<T> is a floating-point type, and this object is initialized to the integer value obtained by discarding the fractional part of x. Initializes the allocator to a.

19 Throws: Nothing if the effective width of the integer value this object is initialized with is less than or equal to inplace_bits; otherwise, exceptions thrown during allocation.

template <input_iterator I, sentinel_for<I> S> requires signed-or-unsigned<iter_value_t<I>> constexpr basic_big_int(I begin, S end, const Allocator& a = Allocator());

20 Effects: Initializes the integer value to an integer value formed by concatenating the base-2 representation of each element in the range [begin, end), where the first element in that range holds the least significant part of the concatenated base-2 representation. If iter_value_t<I> 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 a.

21 Throws: Nothing if the effective width of the combined integer value is less than or equal to inplace_bits; otherwise, exceptions thrown during allocation.

22 Complexity: Linear in the size of [begin, end).

template<input_range R> requires signed-or-unsigned<ranges::range_value_t<R>> constexpr basic_big_int(from_range_t, R&& r, const Allocator& a = Allocator());

23 Effects: Equivalent to: basic_big_int(ranges::begin(r), ranges::end(r), a).

[big.int.ops]

X.3.5 Operations [big.int.ops]

constexpr span<const uint_multiprecision_t> representation() const noexcept;

1 Returns: A span 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 size() of the result is representation_size().

div_to_pos_inf is added by [P3724R3]. I would expect it to be available by the time big_int is standardized.

2 Complexity: Constant.

3 Remarks: If the integer value is greater than or equal to zero, basic_big_int(from_range, representation()) has the same integer value; otherwise, -basic_big_int(from_range, representation()) has the same integer value.
[Note: Consequently, elements of type uint_multiprecision_t 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]

This getter single-handedly imposes a huge amount of constraints on the implementation:

  • basic_big_int needs to store a union of dynamically allocated data and of uint_multiprecision_t to make the value accessible via span.
  • The sign bit is kept separate.
  • The padding needs to be kept zero.
constexpr size_type size() const noexcept;

4 Returns: If the integer value is zero, 0; otherwise ⌊ log2 | v | ⌋ + 1 , where v is the integer value.

The result is identical to std::bit_width(U(std::abs(T(v)))) for a hypothetical signed integer type T with infinite range and a hypothetical unsigned integer type U 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.

constexpr size_type representation_size() const noexcept;

6 Returns: If the integer value is zero, 1; otherwise div_to_pos_inf(size(), numeric_­limits<uint_­multiprecision_t>::digits).

7 Complexity: Constant.

constexpr size_type max_size() const noexcept;

8 Returns: max_representation_size() * numeric_limits<uint_multiprecision_t>::digits.

9 Complexity: Constant.

constexpr size_type max_representation_size() const noexcept;

10 Returns: The maximum number of uint_multiprecision_t objects that can be part of the representation. The result is greater than or equal to inplace_representation_capacity and sufficiently low for max_size() to be the mathematical product of max_representation_size() and numeric_limits<uint_multiprecision_t>::digits.

11 Complexity: Constant.

constexpr size_type capacity() const noexcept;

12 Returns: representation_capacity() * numeric_limits<uint_multiprecision_t>::digits.

13 Complexity: Constant.

constexpr size_type representation_capacity() const noexcept;

14 Returns: max(inplace_representation_capacity, dynamic-representation-capacity() * numeric_­limits<uint_­multiprecision_t>::digits), where dynamic-representation-capacity() is the number of currently allocated uint_­multiprecision_t objects.

15 Complexity: Constant.

constexpr allocator_type get_allocator() const noexcept;

16 Returns: The allocator of this object.

17 Complexity: Constant.

constexpr void reserve(size_type n);

18 Effects: A directive that informs a basic_big_int 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 reserve.

19 Postconditions: capacity() is greater or equal to the argument of reserve if reallocation happens; and equal to the previous value of capacity() otherwise.

constexpr void reserve_representation(size_type n);

20 Effects: Equivalent to: reserve(n * numeric_limits<uint_multiprecision_t>::digits);

constexpr void shrink_to_fit();

21 Effects: If the effective width of the integer value is less than or equal to inplace_bits, shrink_to_fit frees the allocation and stores the integer value within the basic_big_int object. Otherwise, shrink_to_fit is a non-binding request to reduce capacity() to size(). It does not increase capacity(), but may reduce capacity() 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]

constexpr basic_big_int& operator=(const basic_big_int& x);

1 Effects: Sets the integer value to that of x. If allocator_traits<Allocator>::propagate_­on_­container_copy_assignment::value is true, assigns x.get_allocator() to the allocator of this object.

2 Returns: *this.

3 Complexity: Linear in the size of the representation of x.

constexpr basic_big_int& operator=(basic_big_int&& x) noexcept;

4 Effects: Sets the integer value to that of x. If allocator_traits<Allocator>::propagate_­on_­container_move_assignment::value is true, assigns x.get_allocator() to the allocator of this object.

5 Returns: *this.

6 Complexity: Constant.

template<arbitrary-integer T> constexpr basic_big_int& operator=(T&& x) noexcept(no-alloc-constructible-from<T>);

7 Constraints: is_same_v<basic_big_int, remove_cvref_t<T>> is false.

8 Effects: Sets the integer value to that of x.

9 Returns: *this.

template<common-big-int-type-with<basic_big_int> T> constexpr basic_big_int& operator+=(T&& x); template<common-big-int-type-with<basic_big_int> T> constexpr basic_big_int& operator-=(T&& x); template<common-big-int-type-with<basic_big_int> T> constexpr basic_big_int& operator*=(T&& x); template<common-big-int-type-with<basic_big_int> T> constexpr basic_big_int& operator/=(T&& x); template<common-big-int-type-with<basic_big_int> T> constexpr basic_big_int& operator%=(T&& x); template<common-big-int-type-with<basic_big_int> T> constexpr basic_big_int& operator&=(T&& x); template<common-big-int-type-with<basic_big_int> T> constexpr basic_big_int& operator|=(T&& x); template<common-big-int-type-with<basic_big_int> T> constexpr basic_big_int& operator^=(T&& x);

10 Effects: Equivalent to:

*this = std::move(*this) @ std::forward<T>(x); return *this;

where @ is a placeholder for the token in the respective operator@=.

template<signed-or-unsigned S> constexpr basic_big_int& operator<<=(S s);

11 Effects: Equivalent to: return *this = std::move(*this) << s;

template<signed-or-unsigned S> constexpr basic_big_int& operator>>=(S s);

12 Effects: Equivalent to: return *this = std::move(*this) >> s;

constexpr void swap(basic_big_int& x) noexcept(allocator_traits<Allocator>::propagate_on_container_swap::value || allocator_traits<Allocator>::is_always_equal::value);

13 Effects: Exchanges the integer values of this object and of x. If allocator_traits<Allocator>::propagate_­on_­container_swap::value is true, exchanges x.get_allocator() and the allocator of this object.

14 Complexity: Constant.

[big.int.conv]

X.3.7 Conversions [big.int.conv]

template<class T> constexpr explicit operator T() const noexcept;

1 Let v be a prvalue equal to the integer value of this object, of a hypothetical signed integer type with sufficient range to represent v.

2 Constraints: T is a cv-unqualified arithmetic type ([basic.fundamental]).

3 Returns: static_cast<T>(v).
[Note: If T is bool, the result is true if *this has nonzero integer value and false otherwise ([conv.integral]). If T 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]

operator+ are worded as equivalences because we want to inherit everything (including complexity requirements) from constructors.

constexpr basic_big_int operator+() const&;

1 Effects: Equivalent to: return *this;

constexpr basic_big_int operator+() && noexcept;

2 Effects: Equivalent to: return std::move(*this);

operator- and operator~ 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.

constexpr basic_big_int operator-() const&;

3 Returns: 0 - *this.

4 Complexity: Linear in the size of the representation of *this.

constexpr basic_big_int operator-() && noexcept;

5 Returns: 0 - std::move(*this).

6 Complexity: Constant.

7 [Note: The contents of the representation of the result are identical to those of representation() prior to the call. — end note]

constexpr basic_big_int operator~() const&;

8 Returns: -*this - -1.

9 Complexity: Linear in the size of the representation of *this.

constexpr basic_big_int operator~() &&;

10 Returns: -std::move(*this) - 1.

11 Complexity: Linear in the size of the representation of *this.

Prefix operator++ and operator-- are worded as equivalences because this inherits everything. Postfix operators are defaulted.

constexpr basic_big_int& operator++();

12 Effects: Equivalent to: return *this += 1;

constexpr basic_big_int& operator--();

13 Effects: Equivalent to: return *this -= 1;

[big.int.alias]

X.3.9 Alias big_int [big.int.alias]

using big_int = basic_big_int<see below>;

1 Result: A specialization of basic_big_int with an implementation-defined argument B for the min_inplace_capacity constant template parameter, chosen so that B is greater than or equal to numeric_limits<uint_multiprecision_t>::digits and B equals basic_big_int<B>::inplace_­bits.

2 Recommended practice: B should be sufficiently large so that big_int may represent the value of all commonly used integer types without allocating.

big_int::min_inplace_bits is typically 64 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 128, 192, 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]

template<class L, common-big-int-type-with<L> R> constexpr bool operator==(const L& x, const R& y) noexcept;

1 Returns: true if the integer value of x is equal to the integer value of y, and false otherwise.

2 Complexity: Linear in the minimum of the representation sizes of x and y.

template<class L, common-big-int-type-with<L> R> constexpr strong_ordering operator<=>(const L& x, const R& y) noexcept;

3 Returns: strong_ordering::less if the integer value of x is less than the integer value of y, strong_ordering::greater if the integer value of x is greater than the integer value of y, and strong_ordering::equal otherwise.

4 Complexity: Linear in the minimum of the representation sizes of x and y.

The noexcept requirement means that it's not a valid implementation strategy to wrap any integer in basic_big_int 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 == __int128.

[big.int.binary]

X.3.11 Binary operations [big.int.binary]

template<class L, class R> constexpr common-big-int-type<L, R> operator+(L&& x, R&& y); template<class L, class R> constexpr common-big-int-type<L, R> operator-(L&& x, R&& y); template<class L, class R> constexpr common-big-int-type<L, R> operator*(L&& x, R&& y); template<class L, class R> constexpr common-big-int-type<L, R> operator/(L&& x, R&& y); template<class L, class R> constexpr common-big-int-type<L, R> operator%(L&& x, R&& y); template<class L, class R> constexpr common-big-int-type<L, R> operator&(L&& x, R&& y); template<class L, class R> constexpr common-big-int-type<L, R> operator|(L&& x, R&& y); template<class L, class R> constexpr common-big-int-type<L, R> operator^(L&& x, R&& y);

1 Preconditions: For operator/ and operator%, the integer value of y is nonzero.

2 Returns: For each operator function template operator@ where @ is a placeholder for the token in the respective operator@, equivalent to:

return big-int-combine([](auto x_int, auto y_int) { return x_int @ y_int; }, std::forward<L>(x), std::forward<R>(y));

[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 n be the representation size of x and let m be the representation size of y.

4 Complexity: For operator+, operator-, operator&, operator|, and operator^, O( max( n,m ) ) . For operator*, operator/, and operator%, O( nm ) .

template<class T, signed-or-unsigned S> constexpr remove_cvref_t<T> operator<<(T&& x, S s); template<class T, signed-or-unsigned S> constexpr remove_cvref_t<T> operator>>(T&& x, S s);

5 Let @ be << for the first overload and >> for the second overload.

6 Constraints: remove_cvref_t<T> is a specialization of basic_big_int.

7 Preconditions: s is greater or equal to zero.

8 Effects: Equivalent to:

return big-int-combine([](auto x_int, auto shift) { return x_int @ shift; }, std::forward<T>(x), s);

9 Complexity: Linear in the size of the representation of x.

[big.int.hash]

X.3.12 Hash support [big.int.hash]

template<size_t b, class L, class A> struct hash<basic_big_int<b, L, A>>;

1 The specialization is enabled ([unord.hash]).

2 Remarks: Let o1 be an object of type basic_big_int<b1, L1, A1>, and let o2 be an object of type basic_big_int<b2, L2, A2>. If o1 and o2 have equal integer value ([big.int.class]), then hash<basic_big_int<b1, L1, A1>>()(o1) equals hash<basic_big_int<b2, L2, A2>>()(o2).

[Note: For an object x of integral type T, hash<T>()(x) can be unequal to hash<big_int>()(big_int(x)). — end note]

For example, in libc++, hash<int>()(5) is not equal to hash<__int128>()(5), so hash parity with all integral types is not possible.

3 Let o be an object of type basic_big_int<b, L, A>, and let N be the width of the smallest hypothetical signed integer type that can represent the integer value of o. If the implementation provides a type bit-int<N> to provide compatibility with type _BitInt(N) defined in ISO/IEC 9899:2024, then hash<basic_big_int<b, L, A>>()(o) equals hash<bit-int<N>>()(o).

In other words: hashing std::big_int is the same as casting to _BitInt and hashing that.

See also §3.12.1. Hash support for std::big_int.

[big.int.fmt]

X.3.13 Formatter [big.int.fmt]

template<size_t b, class L, class A, class charT> struct formatter<basic_big_int<b, L, A>, charT> { template <class ParseContext> constexpr typename ParseContext::iterator parse(ParseContext& ctx); template<class FormatContext> constexpr typename FormatContext::iterator format(const basic_big_int<b, L, A>& x, FormatContext& ctx) const; };

1 The specialization is enabled and constexpr-enabled ([format.formatter.spec]).

2 The parse member function interprets the format specification as a std-format-spec.

3 For the purposes of [format.string.std], basic_big_int<b, L, A> is treated as a signed integer type.

[Note: As explained in [format.string.std], character output is performed using to_chars. — end note]

[big.int.literal]

X.3.14 Literals [big.int.literal]

template<char... digits> constexpr big_int operator""n() noexcept(see below); template<char... digits> constexpr big_int operator""N() noexcept(see below);

1 Let s be a character sequence obtained by concatenating the elements of digits.

2 Mandates: s matches the syntax of an integer-literal with no integer-suffix ([lex.icon]).

3 Returns: A big_int object whose integer value is that of s interpreted as an integer-literal.

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 big_int::inplace_bits.

The Remarks: element implies we have to perform two-stage parsing. We first parse the literal and see if it can be represented as big_int with SBO. If so, the specialization is noexcept. Otherwise, each invocation of the UDL needs to allocate memory.

Our reference implementation strategy is to pre-parse a static constexpr uint_multiprecision_t limbs[], the size of which can be approximated using sizeof...(digits).

[charconv]

[charconv.syn]

Change the synopsis [charconv.syn] as follows:

#include <big_int> // see [big.int] namespace std { […] constexpr to_chars_result to_chars(char* first, char* last, // freestanding integer-type value, int base = 10); template<size_t b, class L, class A> // freestanding constexpr to_chars_result to_chars(char* first, char* last, const basic_big_int<b, L, A>& value, int base = 10); template<size_t b, class L, class A> // freestanding constexpr to_chars_result to_chars(char* first, char* last, basic_big_int<b, L, A>&& value, int base = 10); to_chars_result to_chars(char* first, char* last, // freestanding bool value, int base = 10) = delete; […] constexpr from_chars_result from_chars(const char* first, const char* last, // freestanding integer-type& value, int base = 10); template<size_t b, class L, class A> // freestanding constexpr from_chars_result from_chars(const char* first, const char* last, basic_big_int<b, L, A>& value, int base = 10); from_chars_result from_chars(const char* first, const char* last, // freestanding-deleted floating-point-type& value, chars_format fmt = chars_format::general); }

[charconv.to.chars]

Change [charconv.to.chars] as follows:

[…]

constexpr to_chars_result to_chars(char* first, char* last, integer-type value, int base = 10); template<size_t b, class L, class A> constexpr to_chars_result to_chars(char* first, char* last, const basic_big_int<b, L, A>& value, int base = 10); template<size_t b, class L, class A> constexpr to_chars_result to_chars(char* first, char* last, basic_big_int<b, L, A>&& value, int base = 10);

4 Constraints: remove_cvref_t<T> is a specialization of basic_big_int.

5 Preconditions: base has a value between 2 and 36 (inclusive).

6 Effects: The integer value ([big.int.class]) of value 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 a..z. If value is less than zero, the representation starts with '-'.

7 Throws: Nothing for functions with a parameter of type integer-value. The overloads for basic_big_int may throw exceptions during allocation unless base is a power of two.

8 Remarks: If base is a power of two, no allocation takes place.

[P4168R0] overhauls this specification.

[charconv.from.chars]

Change [charconv.from.chars] as follows:

[…]

constexpr from_chars_result from_chars(const char* first, const char* last, integer-type& value, int base = 10); template<size_t b, class L, class A> constexpr from_chars_result from_chars(const char* first, const char* last, basic_big_int<b, L, A>& value, int base = 10);

2 Preconditions: base has a value between 2 and 36 (inclusive).

3 Effects: The pattern is the expected form of the subject sequence in the "C" locale for the given nonzero base, as described for strtol, except that no "0b" or "0B" prefix shall appear if the value of base is 2, no "0x" or "0X" 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 integer-value&. The overload for basic_big_int may throw exceptions during allocation.

5 Remarks: If base is a power of two, at most one allocation takes place.

Notice that there exists no wording that describes how the character sequence is interpreted as an integer value (neither for integer-type nor for basic_big_int). 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 <cstdlib> and , <cmath>, and <big_int> declare the functions described in this subclause, but only <big_int> declares the overloads for basic_big_int. — end note]

constexpr int abs(int j); constexpr long int abs(long int j); constexpr long long int abs(long long int j);

2 Effects: […]

3 Remarks: […]

template<size_t b, class L, class A> constexpr basic_big_int<b, L, A> abs(const basic_big_int<b, L, A>& j);

4 Returns: j < 0 ? -j : j.

5 Complexity: Linear in the size of the representation of j if j < 0 is true; otherwise constant.

template<size_t b, class L, class A> constexpr basic_big_int<b, L, A> abs(basic_big_int<b, L, A>&& j) noexcept;

6 Returns: j < 0 ? -std::move(j) : std::move(j).

7 Complexity: Constant.

See also §3.12.4. std::abs.

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.

9. References

[N1692] M.J. Kronenburg. A Proposal to add the Infinite Precision Integer to the C++ Standard Library 2004-07-01 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1692.pdf
[N1744] Michiel Salters. Big Integer Library Proposal for C++0x 2005-01-13 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1744.pdf
[N2143] M.J. Kronenburg. Proposal for an Infinite Precision Integer for Library Technical Report 2 2007-01-11 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2143.pdf
[N4035] Joel Falcou, Peter Gottschling, Herb Sutter. Implicit Evaluation of “auto” Variables and Arguments 2014-05-23 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4035.html
[N4038] Pete Becker. Proposal for Unbounded-Precision Integer Types 2014-05-23 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4038.html
[N5032] Thomas Köppe. Working Draft Programming Languages — C++ 2025-12-15 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/n5032.pdf
[P1974R1] Jeff Snyder, Daveed Vandevoorde. Persistent constexpr allocation 2026-06-09 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p1974r1.pdf
[P4147R0] Hana Dusíková. constexpr => runtime bridge 2026-05-12 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4147r0.html
[P4168R0] Jan Schultke. Fix defects in floating-point std::from_chars(LWG3081, LWG3082, LWG3456) 2026-04-05 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4168r0.html
[P4340R0] Barry Revzin. Extending constant template parameter support by customizing std::meta::reflect_constant 2026-08-11 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4340r0.html
[Reference-Impl] eisenwave/std-big-int reference implementation https://github.com/eisenwave/std-big-int/
[MSR-TR-2022-17] Daan Leijen. What About the Integer Numbers? – Fast Arithmetic with Tagged Integers – A Plea for Hardware Support 2022-07-11 https://www.microsoft.com/en-us/research/wp-content/uploads/2022/07/int.pdf