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_intequal in magnitude tojand not less than0. -
Postconditions: For the lvalue overload,
jis unchanged. For the rvalue overload,jis 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 isnoexcept. -
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:
Ris a signed or unsigned integer type. -
Returns: If the integer value of
xis representable as a value of typeR, that value; otherwise the largest or smallest value representable byR, whichever is closer to the value ofx. In particular, a value greater than the maximum ofRsaturates to that maximum, a value less than the minimum saturates to the minimum, and every negative value yields0whenRis unsigned. -
Throws: Nothing; the function is
noexcept. -
Remarks: The function is usable in a constant expression. The bounds of
Rare derived from its width, so extended integer types and bit-precise integers (_BitInt) are supported even where they lack astd::numeric_limitsspecialization.
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:
Ris a signed or unsigned integer type. -
Returns:
trueif the value ofxis in the range of values that can be represented inR, otherwisefalse. -
Throws: Nothing; the function is
noexcept. -
Remarks: The function is usable in a constant expression. The bounds of
Rare 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, sobool, character types, floating-point types, and a second, unrelatedbasic_big_intspecialization all dropgcdout of overload resolution rather than producing a hard error. Two built-in integers are the business ofstd::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; otherwisegcd(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_intargument. Unlikestd::gcd, no argument can fail to be representable in the result type:basic_big_intis unbounded, so the precondition thatstd::gcdplaces 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 abasic_big_intas an rvalue hands its storage to the computation while an lvalue is left untouched. There are four overloads rather than one because everybasic_big_intdrags namespace std into argument-dependent lookup through its allocator:std::gcdis 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 ofbasic_big_int, and the other is either the same specialization or a signed or unsigned integer type. Anything else dropslcmout of overload resolution rather than producing a hard error, and two built-in integers are the business ofstd::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_intargument, asgcd’s does. Unlike `std::lcm, neither the arguments nor the result can fail to be representable in the result type:basic_big_intis unbounded, so the preconditionsstd::lcmplaces 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 abasic_big_intas an rvalue hands its storage to the computation while an lvalue is left untouched. There are four overloads for the same reasongcdhas four:std::lcmis 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 ofbasic_big_int, and the other is either the same specialization or a signed or unsigned integer type. Anything else dropsmidpointout of overload resolution rather than producing a hard error, and two built-in integers are the business ofstd::midpoint. -
Returns: Half the sum of
mandn. If the sum is odd, the result is rounded towardm. Equivalently, the result ism + (n - m) / 2with 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_intargument, asgcd’s and `lcm’s do. Unlike `std::midpoint, this function is notnoexcept, because a wide result has to be allocated. The guarantee that no overflow occurs is unconditional for the same reason it is vacuous ingcdandlcm:basic_big_intis 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, sincestd::midpointhas 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 reasongcdandlcmhave four:std::midpointis 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 abasic_big_intargument, 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).