Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/modules/ROOT/pages/int128_t.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,10 @@ This operation is subject to mixed sign limitations discussed xref:int128_t.adoc
[#i128_math_operators]
== Arithmetic Operators

All of the arithmetic operators use two's-complement wrap-around on overflow.
When a result is not representable in an `int128_t` it silently rolls over modulo `2^128` rather than trapping or invoking undefined behavior, matching `std::numeric_limits<int128_t>::is_modulo`.
For example, `BOOST_INT128_INT128_MAX + 1` wraps to `BOOST_INT128_INT128_MIN`, and `BOOST_INT128_INT128_MIN - 1` wraps to `BOOST_INT128_INT128_MAX`.

=== Unary Plus and Minus

[source, c++]
Expand Down
45 changes: 39 additions & 6 deletions include/boost/int128/detail/int128_imp.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1883,18 +1883,30 @@ BOOST_INT128_HOST_DEVICE constexpr int128_t operator+(const UnsignedInteger lhs,
return detail::default_add(rhs, lhs);
}

#ifdef _MSC_VER
# pragma warning(push)
# pragma warning(disable : 4146) // Unary minus applied to unsigned type
#endif

BOOST_INT128_EXPORT template <BOOST_INT128_DEFAULTED_SIGNED_INTEGER_CONCEPT>
BOOST_INT128_HOST_DEVICE constexpr int128_t operator+(const int128_t lhs, const SignedInteger rhs) noexcept
{
return rhs > 0 ? detail::default_add(lhs, rhs) : detail::default_sub(lhs, -rhs);
// Negate in the unsigned domain so INT64_MIN does not overflow (UBSAN)
return rhs < 0 ? detail::default_sub(lhs, -static_cast<std::uint64_t>(rhs)) :
detail::default_add(lhs, static_cast<std::uint64_t>(rhs));
}

BOOST_INT128_EXPORT template <BOOST_INT128_DEFAULTED_SIGNED_INTEGER_CONCEPT>
BOOST_INT128_HOST_DEVICE constexpr int128_t operator+(const SignedInteger lhs, const int128_t rhs) noexcept
{
return lhs > 0 ? detail::default_add(rhs, lhs) : detail::default_sub(rhs, -lhs);
return lhs < 0 ? detail::default_sub(rhs, -static_cast<std::uint64_t>(lhs)) :
detail::default_add(rhs, static_cast<std::uint64_t>(lhs));
}

#ifdef _MSC_VER
# pragma warning(pop)
#endif

#if defined(BOOST_INT128_HAS_INT128) || defined(BOOST_INT128_HAS_MSVC_INT128)


Expand Down Expand Up @@ -2272,12 +2284,22 @@ BOOST_INT128_HOST_DEVICE constexpr int128_t operator/(const UnsignedInteger lhs,
else
{
auto abs_rhs {abs(rhs)};
// rhs == -2^64 has |rhs| greater than any 64-bit lhs, so the quotient is 0 (also avoids /0)
if (abs_rhs.high != 0)
{
return {0, 0};
}
const auto res {static_cast<std::uint64_t>(lhs) / abs_rhs.low};
const int128_t result {0, res};
return rhs < 0 ? -result : result;
}
}

#ifdef _MSC_VER
# pragma warning(push)
# pragma warning(disable : 4146) // Unary minus applied to unsigned type
#endif

BOOST_INT128_EXPORT template <BOOST_INT128_DEFAULTED_SIGNED_INTEGER_CONCEPT>
BOOST_INT128_HOST_DEVICE constexpr int128_t operator/(const int128_t lhs, const SignedInteger rhs) noexcept
{
Expand All @@ -2292,15 +2314,16 @@ BOOST_INT128_HOST_DEVICE constexpr int128_t operator/(const int128_t lhs, const

constexpr int128_t min_val {INT64_MIN, 0};
const auto negative_res {static_cast<bool>((lhs.high < 0) ^ (rhs < 0))};
const auto abs_rhs {rhs < 0 ? -rhs : rhs};
// Negate in the unsigned domain so INT64_MIN does not overflow (UBSAN)
const auto abs_rhs {rhs < 0 ? -static_cast<eval_type>(rhs) : static_cast<eval_type>(rhs)};
const auto abs_lhs {abs(lhs)};

if (lhs != min_val && abs_lhs < abs_rhs)
{
return {0, 0};
}

detail::one_word_div(abs_lhs, static_cast<eval_type>(abs_rhs), quotient);
detail::one_word_div(abs_lhs, abs_rhs, quotient);

return negative_res ? -quotient : quotient;
}
Expand All @@ -2321,13 +2344,23 @@ BOOST_INT128_HOST_DEVICE constexpr int128_t operator/(const SignedInteger lhs, c
{
const auto negative_res {static_cast<bool>((rhs.high < 0) ^ (lhs < 0))};
const auto abs_rhs {abs(rhs)};
const auto abs_lhs {lhs < 0 ? -lhs : lhs};
const int128_t res {0, static_cast<std::uint64_t>(abs_lhs) / abs_rhs.low};
// rhs == -2^64 has |rhs| greater than any 64-bit lhs, so the quotient is 0 (also avoids /0)
if (abs_rhs.high != 0)
{
return {0, 0};
}
// Negate in the unsigned domain so INT64_MIN does not overflow (UBSAN)
const auto abs_lhs {lhs < 0 ? -static_cast<std::uint64_t>(lhs) : static_cast<std::uint64_t>(lhs)};
const int128_t res {0, abs_lhs / abs_rhs.low};

return negative_res ? -res : res;
}
}

#ifdef _MSC_VER
# pragma warning(pop)
#endif

#if defined(BOOST_INT128_HAS_INT128) || defined(BOOST_INT128_HAS_MSVC_INT128)


Expand Down
3 changes: 3 additions & 0 deletions test/Jamfile
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ run test_to_string.cpp ;
# Warnings about padding propagate out of <utility>
run test_hash.cpp : : : <toolset>msvc:<cxxflags>/wd4324 ;

run test_boundaries.cpp ;
run test_constexpr_boundaries.cpp ;

# Make sure we run the examples as well
run ../examples/construction.cpp ;
run ../examples/bit.cpp ;
Expand Down
69 changes: 69 additions & 0 deletions test/test_boundaries.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2026 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

#include <boost/int128.hpp>
#include <boost/core/lightweight_test.hpp>
#include <cstdint>
#include <limits>

using boost::int128::int128_t;

namespace
{

std::int64_t opaque(std::int64_t v)
{
volatile std::int64_t s {v};
return s;
}

std::uint64_t opaque(std::uint64_t v)
{
volatile std::uint64_t s {v};
return s;
}

int128_t opaque(int128_t v)
{
return int128_t{opaque(static_cast<std::int64_t>(v.high)), opaque(static_cast<std::uint64_t>(v.low))};
}

} // namespace

int main()
{
const auto int_max {opaque(BOOST_INT128_INT128_MAX)};
const auto int_min {opaque(BOOST_INT128_INT128_MIN)};
const int128_t one {opaque(int128_t{0, 1})};

// Negating a signed scalar of INT64_MIN must not overflow the scalar type.
// The scalar-operand result must equal promoting the scalar to int128_t first.
const std::int64_t m {opaque((std::numeric_limits<std::int64_t>::min)())};
const int128_t m128 {m};
const int128_t a {opaque(int128_t{42, 1234})};
const int128_t b {opaque(int128_t{0, 5})};

BOOST_TEST_EQ(a + m, a + m128); // operator+(int128_t, Signed) was int128_imp.hpp:1889
BOOST_TEST_EQ(m + a, m128 + a); // operator+(Signed, int128_t) was int128_imp.hpp:1895
BOOST_TEST_EQ(a / m, a / m128); // operator/(int128_t, Signed) was int128_imp.hpp:2295
BOOST_TEST_EQ(m / b, m128 / b); // operator/(Signed, int128_t) was int128_imp.hpp:2324

// A 128-bit divisor of exactly -2^64 has abs().low == 0; the quotient of any
// 64-bit numerator by it is 0 and must not divide by zero.
const int128_t neg_two_pow_64 {opaque(int128_t{-1, 0})};
BOOST_TEST_EQ(opaque(std::int64_t{5}) / neg_two_pow_64, (int128_t{0, 0})); // operator/(Signed, int128_t)
BOOST_TEST_EQ(opaque(std::int64_t{-5}) / neg_two_pow_64, (int128_t{0, 0}));
BOOST_TEST_EQ(opaque(std::uint64_t{5}) / neg_two_pow_64, (int128_t{0, 0})); // operator/(Unsigned, int128_t)

// Two's-complement rollover at the range boundaries: no UB, wraps like __int128.
BOOST_TEST_EQ(int_max + one, int_min); // MAX + 1 -> MIN
BOOST_TEST_EQ(int_min - one, int_max); // MIN - 1 -> MAX
BOOST_TEST_EQ(int_max + opaque(std::int64_t{1}), int_min); // MAX + 1 (scalar) -> MIN
BOOST_TEST_EQ(int_min - opaque(std::int64_t{1}), int_max); // MIN - 1 (scalar) -> MAX
BOOST_TEST_EQ(-int_min, int_min); // -MIN wraps to itself
BOOST_TEST_EQ(int_min * opaque(std::int64_t{-1}), int_min); // MIN * -1 overflows to MIN
BOOST_TEST_EQ(int_max * opaque(std::int64_t{2}), (-int128_t{0, 2})); // MAX * 2 overflows to -2

return boost::report_errors();
}
61 changes: 61 additions & 0 deletions test/test_constexpr_boundaries.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2026 Matt Borland
// Distributed under the Boost Software License, Version 1.0.
// https://www.boost.org/LICENSE_1_0.txt

// Compile-time counterpart of test_boundaries.cpp. Every operation is evaluated
// in a constant expression. Undefined behavior is not permitted in a constant
// expression, so any UB here is a compilation error, and the equalities are
// verified at compile time as well.

#include <boost/int128.hpp>
#include <cstdint>
#include <limits>

#ifdef _MSC_VER
# pragma warning(disable : 4307) // integral constant overflow
# pragma warning(disable : 4146) // unary minus operator applied to unsigned type, result still unsigned
#endif

using boost::int128::int128_t;

namespace
{

constexpr auto int_max {BOOST_INT128_INT128_MAX};
constexpr auto int_min {BOOST_INT128_INT128_MIN};
constexpr int128_t one {0, 1};

constexpr std::int64_t m {(std::numeric_limits<std::int64_t>::min)()};
constexpr int128_t m128 {m};
constexpr int128_t a {42, 1234};
constexpr int128_t b {0, 5};
constexpr int128_t neg_two_pow_64 {-1, 0};

// Signed-scalar negation paths (INT64_MIN) must be UB-free and must agree with
// promoting the scalar to int128_t first.
static_assert(a + m == a + m128, "operator+(int128_t, Signed)");
static_assert(m + a == m128 + a, "operator+(Signed, int128_t)");
static_assert(a / m == a / m128, "operator/(int128_t, Signed)");
static_assert(m / b == m128 / b, "operator/(Signed, int128_t)");

// A 128-bit divisor of exactly -2^64 has abs().low == 0; the quotient is 0 and
// must not divide by zero.
static_assert(std::int64_t{5} / neg_two_pow_64 == int128_t{0, 0}, "operator/(Signed, int128_t) -2^64");
static_assert(std::int64_t{-5} / neg_two_pow_64 == int128_t{0, 0}, "operator/(Signed, int128_t) -2^64 negative");
static_assert(std::uint64_t{5} / neg_two_pow_64 == int128_t{0, 0}, "operator/(Unsigned, int128_t) -2^64");

// Two's-complement rollover at the range boundaries: no UB, wraps like __int128.
static_assert(int_max + one == int_min, "MAX + 1 -> MIN");
static_assert(int_min - one == int_max, "MIN - 1 -> MAX");
static_assert(int_max + 1 == int_min, "MAX + 1 (scalar) -> MIN");
static_assert(int_min - 1 == int_max, "MIN - 1 (scalar) -> MAX");
static_assert(-int_min == int_min, "-MIN wraps to itself");
static_assert(int_min * -1 == int_min, "MIN * -1 overflows to MIN");
static_assert(int_max * 2 == -int128_t{0, 2}, "MAX * 2 overflows to -2");

} // namespace

int main()
{
return 0;
}
Loading