mirror of
https://github.com/mpusz/mp-units.git
synced 2026-07-06 00:20:52 +02:00
fc7eb739e7
`double_width_int` was previously missing a number of operators that generic numerical code
expects from a 128-bit integer. On platforms with native `__int128` this was harmless, but on
MSVC (where `int128_t` / `uint128_t` alias `double_width_int<(u)int64_t>`) the recent
`UsesIntegerScaling` change wired the synthetic dwint into concept checks, and `compare_quantities`
+ magnitude folding into runtime paths, exposing every gap.
This change rounds out the operator set so `double_width_int` truly behaves as an integer type:
* binary `+`, `-`, `*`, `/` between two `double_width_int`s (alongside the existing narrow-rhs
overloads).
* unary `~` and binary `&`, `|`, `^`.
* compound assignment for arithmetic, bitwise, and shift operations.
* pre/post `++` and `--`.
* `static_cast<long double>` (and `double`/`float`) with sign-preserving conversion that avoids
catastrophic cancellation on platforms where `long double == double`.
* `std::numeric_limits<double_width_int<T>>` specialization so generic code probing `::max()`,
`::min()`, `::digits`, `::is_signed`, etc. gets correct answers (needed by
`checked_int_pow`, `compute_base_power`, `safe_int::operator-`).
* fields `hi_` / `lo_` and the `(hi, lo)` ctor are public so cross-instance inline friend
operators can access each other without further friend declarations.
While here, also:
* extract `double_width_int` (and its `std::numeric_limits` specialization) into a dedicated
header `bits/double_width_int.h`; `fixed_point.h` now just includes it and keeps the
`int128_t` aliases, `min_width_uint_t` / `double_width_int_for_t` / `wide_product_of`
helpers, and the `fixed_point` class itself.
* rename a local variable `m` in `wide_product_of` to `mid` to avoid shadowing
`si::unit_symbols::m` (MSVC C4459).
* rewrite the `lo_ > 0 ? -1 : 0` unary-minus body to use `Tl{0} - lo_` instead of `-lo_`,
silencing MSVC C4146 about unary minus on an unsigned operand.
Tests:
* new `test/static/double_width_int_test.cpp` pins down each new operator at compile time
(carry/borrow edges, schoolbook multiplication, narrow/wide division paths, bitwise
identities, numeric_limits values, long-double round-trips).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
141 lines
6.3 KiB
C++
141 lines
6.3 KiB
C++
// The MIT License (MIT)
|
|
//
|
|
// Copyright (c) 2018 Mateusz Pusz
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in all
|
|
// copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
|
|
#include <mp-units/bits/double_width_int.h>
|
|
#ifdef MP_UNITS_IMPORT_STD
|
|
import std;
|
|
#else
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <type_traits>
|
|
#endif
|
|
|
|
using namespace mp_units;
|
|
|
|
namespace {
|
|
|
|
using i128 = detail::double_width_int<std::int64_t>;
|
|
using u128 = detail::double_width_int<std::uint64_t>;
|
|
using i32x2 = detail::double_width_int<std::int32_t>;
|
|
using u32x2 = detail::double_width_int<std::uint32_t>;
|
|
|
|
// Shift/divide round-trip on i128 (exercises the narrow-rhs paths)
|
|
static_assert((((83 * 79 * 73) * (i128{97} << 64u) / 89) >> 64u) == (83 * 79 * 73 * 97) / 89);
|
|
|
|
// ---- Arithmetic between two double-width operands ----
|
|
// These compile-time checks pin down the operators that the synthetic int128 emulation must
|
|
// provide on platforms without native __int128 (issue #795).
|
|
|
|
// + with carry from the low half to the high half
|
|
static_assert(u128{1, 2} + u128{3, 4} == u128{4, 6});
|
|
static_assert(u128{0, ~std::uint64_t{0}} + u128{0, 1} == u128{1, 0});
|
|
static_assert(i128{0, ~std::uint64_t{0}} + i128{0, 1} == i128{1, 0});
|
|
|
|
// - with borrow from the high half to the low half
|
|
static_assert(u128{4, 6} - u128{3, 4} == u128{1, 2});
|
|
static_assert(u128{1, 0} - u128{0, 1} == u128{0, ~std::uint64_t{0}});
|
|
static_assert(-i128{0, 1} == i128{-1, ~std::uint64_t{0}});
|
|
|
|
// * truncated to width bits; the high*high contribution overflows and is discarded
|
|
static_assert(u128{0, 5} * u128{0, 3} == u128{0, 15});
|
|
static_assert(u128{0, ~std::uint64_t{0}} * u128{0, 2} == u128{1, ~std::uint64_t{0} - 1}); // 2 * (2^64 - 1)
|
|
static_assert(u128{0, 1u << 16u} * u128{0, 1u << 16u} == u128{0, std::uint64_t{1} << 32u});
|
|
static_assert(i128{0, 5} * i128{-1, ~std::uint64_t{0} - 2} == i128{-1, ~std::uint64_t{0} - 14}); // 5 * -3 = -15
|
|
|
|
// / between two dwints — narrow fast path and wide long-division path
|
|
static_assert(u128{0, 100} / u128{0, 7} == u128{0, 14});
|
|
static_assert(u128{1, 0} / u128{0, 2} == u128{0, std::uint64_t{1} << 63u}); // 2^64 / 2 == 2^63
|
|
static_assert(i128{0, 100} / i128{-1, ~std::uint64_t{0} - 6} == i128{-1, ~std::uint64_t{0} - 13}); // 100 / -7 == -14
|
|
static_assert(u128{1, 5} / u128{2, 0} == u128{0, 0}); // divisor wider than dividend → 0
|
|
|
|
// Bitwise operators
|
|
static_assert(~u128{0, 0} == u128{~std::uint64_t{0}, ~std::uint64_t{0}});
|
|
static_assert(~u128{~std::uint64_t{0}, ~std::uint64_t{0}} == u128{0, 0});
|
|
static_assert((u128{0xFF, 0xFF} & u128{0x0F, 0xF0}) == u128{0x0F, 0xF0});
|
|
static_assert((u128{0xF0, 0x00} | u128{0x0F, 0xFF}) == u128{0xFF, 0xFF});
|
|
static_assert((u128{0xFF, 0x00} ^ u128{0x0F, 0xFF}) == u128{0xF0, 0xFF});
|
|
|
|
// Compound assignment operators (each verified by reading back the value after one operation)
|
|
constexpr auto check_compound = [] {
|
|
u128 v{1, 2};
|
|
v += u128{2, 3};
|
|
if (!(v == u128{3, 5})) return false;
|
|
v -= u128{1, 1};
|
|
if (!(v == u128{2, 4})) return false;
|
|
v *= u128{0, 3};
|
|
if (!(v == u128{6, 12})) return false;
|
|
v /= u128{0, 2};
|
|
if (!(v == u128{3, 6})) return false;
|
|
v &= u128{~std::uint64_t{0}, 0xF};
|
|
if (!(v == u128{3, 6})) return false;
|
|
v |= u128{0, 0xF0};
|
|
if (!(v == u128{3, 0xF6})) return false;
|
|
v ^= u128{0, 0xFF};
|
|
if (!(v == u128{3, 9})) return false;
|
|
v <<= 4u;
|
|
if (!(v == u128{0x30, 0x90})) return false;
|
|
v >>= 4u;
|
|
if (!(v == u128{3, 9})) return false;
|
|
return true;
|
|
};
|
|
static_assert(check_compound());
|
|
|
|
// Pre/post increment and decrement
|
|
constexpr auto check_inc_dec = [] {
|
|
u128 v{0, ~std::uint64_t{0}};
|
|
++v;
|
|
if (!(v == u128{1, 0})) return false; // carry across halves
|
|
--v;
|
|
if (!(v == u128{0, ~std::uint64_t{0}})) return false;
|
|
auto post_inc = v++;
|
|
if (!(post_inc == u128{0, ~std::uint64_t{0}} && v == u128{1, 0})) return false;
|
|
auto post_dec = v--;
|
|
if (!(post_dec == u128{1, 0} && v == u128{0, ~std::uint64_t{0}})) return false;
|
|
return true;
|
|
};
|
|
static_assert(check_inc_dec());
|
|
|
|
// numeric_limits specialization (required by checked_int_pow etc.)
|
|
static_assert(std::numeric_limits<u128>::is_specialized);
|
|
static_assert(std::numeric_limits<u128>::is_integer);
|
|
static_assert(std::numeric_limits<u128>::is_exact);
|
|
static_assert(!std::numeric_limits<u128>::is_signed);
|
|
static_assert(std::numeric_limits<i128>::is_signed);
|
|
static_assert(std::numeric_limits<u128>::digits == 128);
|
|
static_assert(std::numeric_limits<i128>::digits == 127);
|
|
static_assert(std::numeric_limits<u32x2>::digits == 64);
|
|
static_assert(std::numeric_limits<i32x2>::digits == 63);
|
|
static_assert(std::numeric_limits<u128>::radix == 2);
|
|
static_assert(std::numeric_limits<u128>::min() == u128{0, 0});
|
|
static_assert(std::numeric_limits<i128>::min() == i128{std::numeric_limits<std::int64_t>::min(), 0});
|
|
static_assert(std::numeric_limits<u128>::max() == u128{~std::uint64_t{0}, ~std::uint64_t{0}});
|
|
static_assert(std::numeric_limits<i128>::max() == i128{std::numeric_limits<std::int64_t>::max(), ~std::uint64_t{0}});
|
|
|
|
// Conversion to long double (round-trips small integers exactly; the signed branch goes
|
|
// through unary minus to avoid catastrophic cancellation on platforms where long double
|
|
// has fewer than `width` bits of mantissa, e.g. MSVC where long double == double).
|
|
static_assert(static_cast<long double>(u128{0, 12345}) == 12345.0L);
|
|
static_assert(static_cast<long double>(i128{0, 1}) == 1.0L);
|
|
static_assert(static_cast<long double>(-i128{0, 1}) == -1.0L);
|
|
|
|
} // namespace
|