Numeric Functions

beman::big_int provides non-member numeric functions that operate on basic_big_int values. They live in the beman::big_int namespace and are found by argument-dependent lookup, so an unqualified abs(x) resolves to them when x is a basic_big_int. Include them with <beman/big_int/numeric.hpp>, or transitively through the umbrella <beman/big_int.hpp>.

Synopsis

namespace beman::big_int {

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

template <class R, std::size_t b, class L, class A>
    requires /* R is a signed or unsigned integer type */
constexpr R saturating_cast(const basic_big_int<b, L, A>& x) noexcept;

template <class R, std::size_t b, class L, class A>
    requires /* R is a signed or unsigned integer type */
constexpr bool in_range(const basic_big_int<b, L, A>& x) noexcept;

template <std::size_t b, class L, class A, class N>
constexpr /* common basic_big_int type */ gcd(basic_big_int<b, L, A>&& m, N&& n);
template <std::size_t b, class L, class A, class N>
constexpr /* common basic_big_int type */ gcd(const basic_big_int<b, L, A>& m, N&& n);
template <class M, std::size_t b, class L, class A>
    requires /* M is a signed or unsigned integer type */
constexpr basic_big_int<b, L, A> gcd(M&& m, basic_big_int<b, L, A>&& n);
template <class M, std::size_t b, class L, class A>
    requires /* M is a signed or unsigned integer type */
constexpr basic_big_int<b, L, A> gcd(M&& m, const basic_big_int<b, L, A>& n);

template <std::size_t b, class L, class A, class N>
constexpr /* common basic_big_int type */ lcm(basic_big_int<b, L, A>&& m, N&& n);
template <std::size_t b, class L, class A, class N>
constexpr /* common basic_big_int type */ lcm(const basic_big_int<b, L, A>& m, N&& n);
template <class M, std::size_t b, class L, class A>
    requires /* M is a signed or unsigned integer type */
constexpr basic_big_int<b, L, A> lcm(M&& m, basic_big_int<b, L, A>&& n);
template <class M, std::size_t b, class L, class A>
    requires /* M is a signed or unsigned integer type */
constexpr basic_big_int<b, L, A> lcm(M&& m, const basic_big_int<b, L, A>& n);

template <std::size_t b, class L, class A, class N>
constexpr /* common basic_big_int type */ midpoint(basic_big_int<b, L, A>&& m, N&& n);
template <std::size_t b, class L, class A, class N>
constexpr /* common basic_big_int type */ midpoint(const basic_big_int<b, L, A>& m, N&& n);
template <class M, std::size_t b, class L, class A>
    requires /* M is a signed or unsigned integer type */
constexpr basic_big_int<b, L, A> midpoint(M&& m, basic_big_int<b, L, A>&& n);
template <class M, std::size_t b, class L, class A>
    requires /* M is a signed or unsigned integer type */
constexpr basic_big_int<b, L, A> midpoint(M&& m, const basic_big_int<b, L, A>& n);

} // namespace beman::big_int

abs

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

Returns the absolute value (the magnitude) of j. An lvalue, and a const rvalue, select the first overload; a non-const rvalue selects the second.

Unlike std::abs, this function can never overflow: basic_big_int is unbounded, so the magnitude of every value is representable. In particular abs is total, with no counterpart to the undefined behavior of std::abs(INT_MIN).

basic_big_int keeps a sign apart from its magnitude, so the result differs from j in that one bit alone: no limb is read, written, or reinterpreted, and the cost of abs is the cost of producing the returned object and nothing more. The rvalue overload therefore takes over the storage j already owns and clears the sign in place, which allocates nothing and cannot throw, however wide the value. The lvalue overload has to leave j intact, so it pays for one copy of the magnitude; that copy allocates only when the magnitude does not fit the in-place buffer.

Prefer handing the value over when the caller is finished with it:

big_int x = -(big_int{1} << 4096);

const big_int magnitude = abs(x);            // Copies; x still holds -2^4096.
x                       = abs(std::move(x)); // Clears the sign bit in place; allocates nothing.
  • Returns: A basic_big_int equal in magnitude to j and not less than 0.

  • Postconditions: For the lvalue overload, j is unchanged. For the rvalue overload, j is in a valid but unspecified state.

  • Throws: For the lvalue overload, any exception thrown while allocating the copied result (for example, std::bad_alloc); a magnitude held in the in-place buffer allocates nothing. The rvalue overload is noexcept.

  • Remarks: The function is usable in a constant expression. The result uses the allocator of j.

saturating_cast

template <class R, std::size_t b, class L, class A>
    requires /* R is a signed or unsigned integer type */
constexpr R saturating_cast(const basic_big_int<b, L, A>& x) noexcept;

Converts x to the integer type R, clamping the result to the range of R when the value of x does not fit. This is the saturating counterpart to the explicit operator R() conversion, which instead truncates the value modulo the width of R.

  • Constraints: R is a signed or unsigned integer type.

  • Returns: If the integer value of x is representable as a value of type R, that value; otherwise the largest or smallest value representable by R, whichever is closer to the value of x. In particular, a value greater than the maximum of R saturates to that maximum, a value less than the minimum saturates to the minimum, and every negative value yields 0 when R is unsigned.

  • Throws: Nothing; the function is noexcept.

  • Remarks: The function is usable in a constant expression. The bounds of R are derived from its width, so extended integer types and bit-precise integers (_BitInt) are supported even where they lack a std::numeric_limits specialization.

in_range

template <class R, std::size_t b, class L, class A>
    requires /* R is a signed or unsigned integer type */
constexpr bool in_range(const basic_big_int<b, L, A>& x) noexcept;

Returns the Boolean result of the range-check if the argument can be represented in R. This is the basic_big_int counterpart of std::in_range.

in_range<std::size_t>(local::big_int(-1)); // false
in_range<std::size_t>(local::big_int(42)); // true

in_range<int>(local::big_int(-1)); // true
in_range<int>(local::big_int(42)); // true
  • Constraints: R is a signed or unsigned integer type.

  • Returns: true if the value of x is in the range of values that can be represented in R, otherwise false.

  • Throws: Nothing; the function is noexcept.

  • Remarks: The function is usable in a constant expression. The bounds of R are derived from its width.

gcd

template <std::size_t b, class L, class A, class N>
constexpr /* common basic_big_int type */ gcd(basic_big_int<b, L, A>&& m, N&& n);
template <std::size_t b, class L, class A, class N>
constexpr /* common basic_big_int type */ gcd(const basic_big_int<b, L, A>& m, N&& n);
template <class M, std::size_t b, class L, class A>
    requires /* M is a signed or unsigned integer type */
constexpr basic_big_int<b, L, A> gcd(M&& m, basic_big_int<b, L, A>&& n);
template <class M, std::size_t b, class L, class A>
    requires /* M is a signed or unsigned integer type */
constexpr basic_big_int<b, L, A> gcd(M&& m, const basic_big_int<b, L, A>& n);

Returns the greatest common divisor of m and n. This is the basic_big_int counterpart of std::gcd, and it accepts any combination of basic_big_int and built-in integer arguments as long as one of the two is a basic_big_int:

const big_int a = 1071_n;

gcd(a, 462_n);         // big_int and big_int
gcd(a, 462);           // big_int and int
gcd(462U, a);          // unsigned and big_int
gcd(a, -462LL);        // signs are ignored
  • Constraints: One operand is a specialization of basic_big_int, and the other is either the same specialization or a signed or unsigned integer type. Whether the other operand is admissible follows from the return type, which is ill-formed otherwise, so bool, character types, floating-point types, and a second, unrelated basic_big_int specialization all drop gcd out of overload resolution rather than producing a hard error. Two built-in integers are the business of std::gcd, not of these overloads.

  • Returns: The greatest common divisor of |m| and |n|. The result is never negative. If both arguments are zero the result is zero; otherwise gcd(x, 0) is |x|.

  • Throws: Any exception thrown while allocating the result or the values the reduction works on (for example, std::bad_alloc). Arguments whose magnitudes both fit a single limb, and an argument pair where one magnitude fits a single limb, are computed without allocating at all — however wide the other operand is, since a borrowed operand is not copied unless the reduction needs a mutable value.

  • Remarks: The function is usable in a constant expression. The result uses the allocator of the basic_big_int argument. Unlike std::gcd, no argument can fail to be representable in the result type: basic_big_int is unbounded, so the precondition that std::gcd places on |m| and |n| is vacuous here. The operands are taken by forwarding reference and classified the way the binary operators classify theirs, so passing a basic_big_int as an rvalue hands its storage to the computation while an lvalue is left untouched. There are four overloads rather than one because every basic_big_int drags namespace std into argument-dependent lookup through its allocator: std::gcd is always a candidate, and spelling the big_int operand as a specialization — something `std::gcd’s plain type parameter cannot deduce — is what makes these the more specialized overloads, and an unqualified call unambiguous.

Algorithm

Operand magnitudes that fit a single limb are reduced by the binary (Stein) algorithm, and a single-limb operand reduces a wide one through one short division, so neither case allocates or divides more than once.

Wider operands are reduced by Lehmer’s algorithm in Jebelean’s double-digit form: the two leading limbs of the pair are run through the Euclidean algorithm on their own, and the cofactors of the steps that are provably steps of the full-precision reduction are applied to the operands with two single-limb multiplications. That trades one division per Euclidean step for a pair of linear passes per limb of progress. Where the leading limbs determine nothing — operands that differ widely in size — a division or a binary step carries the reduction instead.

gcd_bench.test.cpp measures the result against boost::multiprecision::cpp_int, which reduces wide operands with the same algorithm. It is built with the tests and runs when BEMAN_BIG_INT_RUN_BENCHMARKS is defined. On an Apple M4 Max (AppleClang, release build) the two agree to within 1% from 64 to 512 bits and stay within 25% of each other through 4096 bits, while the cases that allocate nothing — a wide operand against a single-limb or built-in integer one — run about twice as fast.

lcm

template <std::size_t b, class L, class A, class N>
constexpr /* common basic_big_int type */ lcm(basic_big_int<b, L, A>&& m, N&& n);
template <std::size_t b, class L, class A, class N>
constexpr /* common basic_big_int type */ lcm(const basic_big_int<b, L, A>& m, N&& n);
template <class M, std::size_t b, class L, class A>
    requires /* M is a signed or unsigned integer type */
constexpr basic_big_int<b, L, A> lcm(M&& m, basic_big_int<b, L, A>&& n);
template <class M, std::size_t b, class L, class A>
    requires /* M is a signed or unsigned integer type */
constexpr basic_big_int<b, L, A> lcm(M&& m, const basic_big_int<b, L, A>& n);

Returns the least common multiple of m and n. This is the basic_big_int counterpart of std::lcm, and like gcd it accepts any combination of basic_big_int and built-in integer arguments as long as one of the two is a basic_big_int:

const big_int a = 1071_n;

lcm(a, 462_n);         // big_int and big_int
lcm(a, 462);           // big_int and int
lcm(462U, a);          // unsigned and big_int
lcm(a, -462LL);        // signs are ignored
  • Constraints: The same as gcd's: one operand is a specialization of basic_big_int, and the other is either the same specialization or a signed or unsigned integer type. Anything else drops lcm out of overload resolution rather than producing a hard error, and two built-in integers are the business of std::lcm.

  • Returns: The least common multiple of |m| and |n|, which is the smallest non-negative value that both operands divide. The result is never negative. If either argument is zero the result is zero, since zero is then the only common multiple.

  • Throws: Any exception thrown while allocating the result or the value the computation works on (for example, std::bad_alloc). Arguments whose magnitudes both fit a single limb are computed without allocating at all when their least common multiple fits a limb as well, and a zero argument allocates nothing either way.

  • Remarks: The function is usable in a constant expression. The result uses the allocator of the basic_big_int argument, as gcd’s does. Unlike `std::lcm, neither the arguments nor the result can fail to be representable in the result type: basic_big_int is unbounded, so the preconditions std::lcm places on |m|, |n|, and the least common multiple itself are all vacuous here — which is the practical reason to reach for this overload set, since the least common multiple of two built-in integers overflows long before the operands do. The operands are taken by forwarding reference, so passing a basic_big_int as an rvalue hands its storage to the computation while an lvalue is left untouched. There are four overloads for the same reason gcd has four: std::lcm is a candidate of every unqualified call through argument-dependent lookup on the allocator, and only a parameter that its plain type parameter cannot deduce makes these the more specialized ones.

Algorithm

There is no reduction of its own. The least common multiple is the product of the two magnitudes with their greatest common divisor taken out once, so the work is one gcd, one exact division, and one multiplication.

The divisor is taken out before the multiplication rather than after, so no intermediate value is wider than the result. Of the two operands, the one with fewer limbs is the one divided: the quotients differ in size, and dividing the narrower operand leaves both the cheaper division and the cheaper multiplication. Where both magnitudes fit a single limb the whole computation is scalar — a single-limb binary gcd, one division, and one widening multiplication — and a result that fits a limb as well stays in the small-object buffer.

lcm_bench.test.cpp measures the result against boost::multiprecision::cpp_int, which composes its own lcm the same way. It is built with the tests and runs when BEMAN_BIG_INT_RUN_BENCHMARKS is defined. On an Apple M4 Max (AppleClang, release build) the two stay within 15% of each other for operands of equal size from 64 to 4096 bits, with or without a common factor planted in both, while a wide operand against a single-limb one runs about 2.5 times faster.

midpoint

template <std::size_t b, class L, class A, class N>
constexpr /* common basic_big_int type */ midpoint(basic_big_int<b, L, A>&& m, N&& n);
template <std::size_t b, class L, class A, class N>
constexpr /* common basic_big_int type */ midpoint(const basic_big_int<b, L, A>& m, N&& n);
template <class M, std::size_t b, class L, class A>
    requires /* M is a signed or unsigned integer type */
constexpr basic_big_int<b, L, A> midpoint(M&& m, basic_big_int<b, L, A>&& n);
template <class M, std::size_t b, class L, class A>
    requires /* M is a signed or unsigned integer type */
constexpr basic_big_int<b, L, A> midpoint(M&& m, const basic_big_int<b, L, A>& n);

Returns half the sum of m and n. This is the basic_big_int counterpart of std::midpoint, and like gcd and lcm it accepts any combination of basic_big_int and built-in integer arguments as long as one of the two is a basic_big_int:

const big_int a = 1071_n;

midpoint(a, 463_n);    // big_int and big_int
midpoint(a, 463);      // big_int and int
midpoint(463U, a);     // unsigned and big_int
midpoint(a, -463LL);   // signs are kept, unlike in gcd and lcm

When the two operands are an odd distance apart the exact midpoint lies half way between two integers, and the result is the one on the side of the first argument. Swapping the arguments therefore moves the result by exactly one:

midpoint(big_int{5}, big_int{8});   //  6
midpoint(big_int{8}, big_int{5});   //  7
midpoint(big_int{-5}, big_int{-8}); // -6
midpoint(big_int{-8}, big_int{-5}); // -7

midpoint(big_int{5}, big_int{9});   //  7, and so is midpoint(9, 5)
  • Constraints: The same as gcd's: one operand is a specialization of basic_big_int, and the other is either the same specialization or a signed or unsigned integer type. Anything else drops midpoint out of overload resolution rather than producing a hard error, and two built-in integers are the business of std::midpoint.

  • Returns: Half the sum of m and n. If the sum is odd, the result is rounded toward m. Equivalently, the result is m + (n - m) / 2 with the division truncated toward zero.

  • Throws: Any exception thrown while allocating the result or the value the arithmetic works on (for example, std::bad_alloc). Arguments whose magnitudes both fit a single limb are computed without allocating at all, whatever their signs, since the midpoint of two such values fits a limb as well.

  • Remarks: The function is usable in a constant expression. The result uses the allocator of the basic_big_int argument, as gcd’s and `lcm’s do. Unlike `std::midpoint, this function is not noexcept, because a wide result has to be allocated. The guarantee that no overflow occurs is unconditional for the same reason it is vacuous in gcd and lcm: basic_big_int is unbounded, so neither the sum of the operands nor the difference between them can fail to be representable — which is the practical reason to reach for this overload set, since std::midpoint has to work around exactly that hazard for built-in types. Only the second operand is taken over when it is passed as an rvalue; the first is read again after the halving, so it is left untouched either way. There are four overloads for the same reason gcd and lcm have four: std::midpoint is a candidate of every unqualified call through argument-dependent lookup on the allocator. Its own constraints admit only arithmetic types, so it drops out on its own for a basic_big_int argument, but spelling the big_int operand as a specialization is still what lets these overloads take the mixed operand pairs that `std::midpoint’s single type parameter cannot deduce.

Algorithm

The result is formed as m + (n - m) / 2 rather than as half the sum, so that no intermediate value is wider than one limb past the wider operand and the rounding falls out of the halving instead of needing a correction. The halving is a one-bit right shift of the magnitude, which truncates toward zero — the operator>> on a basic_big_int rounds toward negative infinity instead, so it is the magnitude, not the value, that is shifted.

Operands whose magnitudes both fit a single limb skip all of that: the midpoint of two single-limb values fits a single limb too, so it is computed with scalar arithmetic on the two magnitudes and built directly, without allocating. Where the operands sit on opposite sides of zero the gap between them is the sum of the magnitudes and can carry out of a limb, so that case halves the gap as it forms it, with (|m| & |n|) + ((|m| ^ |n|) >> 1).