std::big_int

Document number:
D4444R0
Date:
2026-05-24
Audience:
SG6
Project:
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21
Reply-to:
Jan Schultke <janschultke@gmail.com>
GitHub Issue:
wg21.link/P4444/github
Source:
github.com/eisenwave/cpp-proposals/blob/master/src/big-int.cow

This paper is a technical specification of std::big_int. Other sections will be filled in over time.

Contents

1

Introduction

1.1

Infinite-precision integers in other languages

2

Motivation

2.1

std::big_int is a vocabulary type

2.2

std::big_int for convenience and correctness

2.3

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

2.4

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

3

Design

3.1

std::uint_multiprecision_t

3.2

Small object optimizations

3.3

Sign and magnitude

3.4

Naming

4

Implementation experience

5

Wording

5.1

[version.syn]

5.2

[big.int]

5.3

[charconv]

5.4

[numeric.int.div]

5.5

[numeric.abs]

5.6

[numeric.sat.cast]

5.7

[numeric.conversions]

6

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), 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 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
MATLAB Symbolic Math Toolbox standard library
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
Standard ML IntInf standard library
Perl Math::BigInt standard library
PHP GMP and BCMath standard library
R gmp third-party package
Swift BigInt third-party package
Rust num-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:

GitHub code search Amount of files
language:TypeScript /\bbigint/ -is:fork 1.1M
language:Java /\bbigint/ -is:fork 762K
language:JavaScript /\bbigint/ -is:fork 389K
language:C++ /\b(big|cpp)_?u?int/ -is:fork 171K

2. Motivation

In terms of utility, std::big_int has two main use case:

  1. The use case where integers are potentially big. For example, std::big_int could be the result of parsing an integer in a config file, and while the numbers are likely small in practice, it is theoretically possible that the user puts a huge value into the config file. std::big_int is useful in many scenarios because it eliminates the need for range checks, and it eliminates the possibility of undefined behavior through signed integer overflow. One could also describe this as the correctness use case.
  2. The use case where integers are actually big. For example, when performing computations in cryptography, statistics, etc. where the numbers involved simply don't fit into 64-bit integers.

2.1. 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.

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, it is arguably gratuitous as a vocabulary type. By comparison, there is no widely established convention for passing large integers between libraries in C++, and std::big_int could lay that foundation.

Sometimes (such as in the case of loading JSON configs), large integers can be passed as decimal digit strings. However, this is really just a form of delaying deserialization in hopes that the library consumer can do it.

It is not a good way of passing huge integers between libraries in general because conversion to/from strings for huge integers is very expensive.

2.2. 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::cpp_int, they may just not do it.

C++ developers don't get to choose between a fixed-width type for performance and an infinite-precision type for correctness; they get to choose between a fixed-width type and adding a massive dependency for correctness, which they may not do, especially if that dependency is transitively forced onto their library users.

2.3. 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.4. 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(); // 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 provides the obvious 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 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 if this proposal, but it does demonstrate why some compiler support is needed.

3. Design

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 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 capacity>;

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 store in-place or dynamically.

3.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.

Curiously, there is no existing type with the desired properties despite the abundance of type aliases in <cstdint> and <cstddef>:

3.2. 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.

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

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 bis 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

3.3. 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.

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. That is, for example, to compute a & b with negative inputs, the implementation negates the limbs of negative inputs on the fly and also negates the final result if the result is negative.

~x is equivalent to (-x - 1). In bitwise operations, negative inputs are treated as if they were preceded by an infinite sequences of leading ones, and positive inputs are treated as if they were preceded by an infinite sequence of leading zeros.

3.4. 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.

4. Implementation experience

Our reference implementation can be found at [Reference-Impl].

5. Wording

The changes are relative to [N5032].

[version.syn]

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

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

[big.int]

In Clause [numerics], insert a new subclause immediately following [complex.numbers].

X Arbitrary-precision arithmetic [big.int]

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.

X.2 Header <big_int> synopsis [big.int.syn]

#include <compare> #include <span> 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 Allocator = allocator<uint_multiprecision_t>> 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>; }; // [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> remove_cvref_t<T> operator<<(T&& x, S s); template<class T, signed-or-unsigned S> remove_cvref_t<T> operator>>(T&& x, S s); namespace pmr { template<size_t b> using basic_big_int = std::basic_big_int<b, polymorphic_allocator<uint_multiprecision_t>>; using big_int = basic_big_int<std::big_int::inplace_bits>; } // [big.int.hash], hash support template<class T> struct hash; template<size_t b, class A> struct hash<basic_big_int<b, A>>; // [big.int.literal], literals inline namespace literals { inline namespace big_int_literals { template<char... digits> constexpr big_int operator""n() noexcept(see below); } } }

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 single instruction in the execution environment.

It is important not to overconstrain here because there are many edge cases in architectures:

  • 8-bit microcontrollers only have 8-bit arithmetic but can address memory with 16-bit addressing; the appropriate type there is presumably uint8_t despite size_t and int being wider.
  • WASM32 only has 32-bit addressable memory and has a 32-bit size_t; however, WASM32 supports 64-bit arithmetic instructions, so the appropriate limb type there is uint64_t.
  • In most cases, the limb type can simply be size_t.

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

template<size_t min_inplace_capacity, class Allocator> 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 = inplace_representation_capacity * numeric_limits<uint_multiprecision_t>::digits; // [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; 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_range R> requires signed-or-unsigned<ranges::range_value_t<R>> constexpr explicit 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++() &; constexpr basic_big_int operator++(int) &; constexpr basic_big_int& operator--() &; constexpr basic_big_int operator--(int) &; };

1 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.

2 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.

3 Template parameter min_inplace_capacity specifies the minimum width of integers that a basic_big_int shall be capable of representing 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 standard or extended integer type ([basic.fundamental]).

The design aspiration is that basic_big_int is capable of storing any standard or extended integer type without the use of allocations, including __int128 and unsigned __int128. This is easily possible by just putting the proper integer type into a union with the pointer to allocated data.

It is theoretically possible to use a greater limit and store a uint_multiprecision_t[] within the basic_big_int, but this is more difficult to implement than if you only need small object optimizations for a single scalar, so the initial proposal maybe shouldn't mandate it.

4 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 shall not hold an allocation following any operation.
[Note: The behavior is as if shrink_to_fit() was called following every operation that may allocate. — end note]

This ensures that even without non-transient allocations, as long as the value can be stored directly in the object, you can create a constexpr big_int variable. Otherwise, it would be possible to hold e.g. the value 123 in dynamic storage, even though it can be represented without allocations.

Giving the implementation the freedom to hold an unnecessary allocation still makes sense because that allocation may be reused at a later point.

5 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.

X.3.1 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 participate 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).

X.3.2 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.

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

constexpr basic_big_int() noexcept(noexcept(Allocator()));

1 Effects: Initializes the integer value to zero.

constexpr explicit basic_big_int(const Allocator& a) noexcept;

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

constexpr basic_big_int(const basic_big_int& x);

3 Effects: Initializes the integer value to that of x. Initializes the allocator to x.get_allocator().

4 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.

constexpr basic_big_int(basic_big_int&& x) noexcept;

5 Effects: Initializes the integer value to that of x. Initializes the allocator to x.get_allocator().

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

6 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.

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

8 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.

9 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.

10 Remarks: The constructor is explicit if remove_cvref_t<T> is neither a signed or unsigned integer type ([basic.fundamental]) nor basic_big_int<inplace_bits, Allocator>.

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>);

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

12 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.

13 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_range R> requires signed-or-unsigned<ranges::range_value_t<R>> constexpr explicit basic_big_int(from_range_t, R&& r, const Allocator& a = Allocator());

14 Effects: Initializes the integer value to an integer value formed by concatenating the base-2 representation of each element in r, where the first element in r holds the least significant part of the concatenated base-2 representation. If ranges::range_value_t<R> 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.

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

X.3.4 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 div_to_pos_inf(representation_size(), numeric_limits<uint_multiprecision_t>::digits).

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

2 Remarks: If the integer value is greater or equal to zero, basic_big_int(from_range, representation(), get_allocator()) shall have 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;

3 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.

4 Complexity: Constant.

constexpr size_type representation_size() const noexcept;

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

constexpr size_type max_size() const noexcept;

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

constexpr size_type max_representation_size() const noexcept;

7 Returns: The maximum number of uint_multiprecision_t objects that can be part of the representation.

constexpr size_type capacity() const noexcept;

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

constexpr size_type representation_capacity() const noexcept;

9 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.

constexpr allocator_type get_allocator() const noexcept;

10 Returns: The allocator of this object.

constexpr void reserve(size_type n);

11 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.

12 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);

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

constexpr void shrink_to_fit();

14 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.

15 Complexity: If the size is not equal to the old capacity, linear in the size of the sequence; otherwise constant.

X.3.5 Modifiers [big.int.modifiers]

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

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

2 Returns: *this.

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

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

4 Returns: *this.

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

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

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

7 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);

8 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);

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

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

10 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);

11 Effects: Exchanges the integer values of this object and of x.

What does this do for allocators?

X.3.6 Conversions [big.int.conv]

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

1 Let U be a hypothetical signed integer type with sufficient width to represent the basic_big_int's integer value v.

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

3 Returns: static_cast<T>(static_cast<U>(v)).
[Note: If T is bool, the result is true if v is nonzero 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]

X.3.7 Unary operations [big.int.unary]

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);

constexpr basic_big_int operator-() const&;

3 Effects: Equivalent to: return 0 - *this;

constexpr basic_big_int operator-() && noexcept;

4 Effects: Equivalent to: return 0 - std::move(*this);

5 Complexity: Constant.

6 Remarks: The contents of the representation of the result are identical to those of representation() prior to the call.

constexpr basic_big_int operator~() const&;

7 Effects: Equivalent to: return -1 - *this;

constexpr basic_big_int operator~() &&;

8 Returns: Equivalent to: return -1 - std::move(*this);

constexpr basic_big_int& operator++() &;

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

constexpr basic_big_int operator++(int) &;

10 Effects: Equivalent to:

auto copy = *this; ++(*this); return copy;
constexpr basic_big_int& operator--() &;

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

constexpr basic_big_int operator--(int) &;

12 Effects: Equivalent to:

auto copy = *this; --(*this); return copy;

X.3.8 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 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.

X.3.9 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.

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.

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

2 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 y is greater than the integer value of y, and strong_ordering::equal otherwise.

X.3.10 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 respective token, a basic_big_int object whose integer value is that of performing the operation T(x) @ T(y), where T is a hypothetical signed integer type with infinite range.
[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]

std::atomic has a similar specification with operator@; maybe copy the wording from there.

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

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

4 Preconditions: s is greater or equal to zero.

5 Returns: A basic_big_int whose integer value is v×2s, where v is the integer value of x, and whose allocator is that of x.

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

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

7 Preconditions: s is greater or equal to zero.

8 Returns: A basic_big_int whose integer value is v×2s rounded towards negative infinity, where v is the integer value of x, and whose allocator is that of x.

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

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

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

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

X.3.12 Literals [big.int.literal]

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 and containing no digit separators. ([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.

This 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.

Perhaps a good way of implementing this would be to have a variable template template<char... digits> big_int_parsed; which holds std::optional<std::big_int>, where a value is present only if the pre-parsed value fits in std::big_int without allocation.

[charconv]

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 inplace_bits, class Allocator> constexpr to_chars_result to_chars(char* first, char* last, const basic_big_int<inplace_bits, Allocator>& 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 inplace_bits, class Allocator> constexpr from_chars_result from_chars(const char* first, const char* last, basic_big_int<inplace_bits, Allocator>& 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); }

[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);

[numeric.abs]

std::abs currently sits in <cmath> or in [c.math.abs], and it seems a bit absurd to require <cmath> to pull in <big_int>. I think it would make more sense for <numeric> to also expose std::abs instead (it probably already does in many standard libraries), and then extend the overload set:

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

1 Effects: […]

2 Remarks: […]

template<class T> constexpr remove_cvref_t<T> abs(T&& x);

3 Constraints: remove_cvref_t<T> is a specialization of basic_big_int ([bit.int]).

4 Returns: -std::forward<T>(x) if the integer value of x is negative, and std::forward<T>(x) otherwise.

[numeric.sat.cast]

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 A> constexpr R saturating_cast(const basic_big_int<b, 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.

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.

Therefore, saturating_cast can act as an unsurprising spelling.

[numeric.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 A> constexpr string to_string(const basic_big_int<b, A>& val);

Returns: format("{}", 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 A> constexpr wstring to_wstring(const basic_big_int<b, A>& val);

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

This requires formatting support.

6. 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
[N4038] Pete Becker. Big Integer Library Proposal for C++0x 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
[Reference-Impl] eisenwave/std-big-int reference implementation https://github.com/eisenwave/std-big-int/