fix: resolve MSVC C2666 on safe_int<T> × safe_int<U> via Hidden Friend Injection

Adds heterogeneous arithmetic operators (+ - * / %) to `detail::safe_int_binary_ops`,
following the same idiom already used for `==` and `<=>`.  The non-template homogeneous
inline-friends inside `safe_int<T>` keep winning same-T calls; the new template friends
in the base unambiguously handle the T != U calls without changing the converting-ctor
semantics.  Resolves the ambiguity that triggered C2666 on `safe_int<int> + safe_int<long>`
when `int` and `long` have equal width (Windows).

Also:
  * `mul_overflows<T>` now gates on `integer_rep_width_v<int128_t>` so MSVC's synthetic
    `double_width_int<int64_t>` counts as a usable wider type for the overflow check.
  * Replace the platform-dependent `safe_int<long>` widening test with a portable
    `long long` variant — on Windows the original test's RHS
    `static_cast<long>(INT_MAX) + 1L` is itself constexpr-invalid 32-bit overflow.
  * Add convertibility coverage tests for `safe_int<T> op raw_U` mixing short / int /
    long / uint8_t to lock down that the `safe_int op integral` overload wins by exact
    match over the converting ctor on every platform.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Mateusz Pusz
2026-05-24 19:31:09 +02:00
parent 2a4c34253b
commit 01bf044919
2 changed files with 108 additions and 4 deletions
+67 -1
View File
@@ -140,7 +140,14 @@ template<integral T>
template<integral T>
[[nodiscard]] constexpr bool mul_overflows(T lhs, T rhs) noexcept
{
if constexpr (integer_rep_width_v<T> < max_native_width) {
// Use a wider type for the overflow check whenever one exists. On platforms without native
// `__int128`, `int128_t` is the synthetic `double_width_int<int64_t>` — still a usable
// 128-bit type — so `max_native_width` (which only counts builtin widths) is the wrong
// ceiling; check directly against `integer_rep_width_v<int128_t>` instead. The else branch
// below assumes T is the widest available signed integer (i.e. (u)int128_t) and computes
// `max` by truncating uint128_t — that truncation is what produced the bug on MSVC for
// T == long long.
if constexpr (integer_rep_width_v<T> < integer_rep_width_v<int128_t>) {
using wide = double_width_int_for_t<T>;
const wide product = static_cast<wide>(lhs) * static_cast<wide>(rhs);
return product > static_cast<wide>(std::numeric_limits<T>::max()) ||
@@ -336,6 +343,65 @@ struct safe_int_binary_ops {
if (std::cmp_greater(lhs.value_, rhs.value_)) return std::strong_ordering::greater;
return std::strong_ordering::equal;
}
// Heterogeneous arithmetic: safe_int<T,EP> op safe_int<U,EP>, T != U, matching sign.
// Same idiom as the comparison operators above — ADL finds these via the base class,
// the homogeneous (T == U) inline-friend operators inside `safe_int<T>` win for same-T
// calls because non-template beats template in overload resolution, and the
// heterogeneous calls have no competition. Resolves the MSVC C2666 ambiguity on
// `safe_int<int> + safe_int<long>` when `int` and `long` have equal width (Windows).
template<integral T, integral U, typename EP>
requires(!std::same_as<T, U> && same_sign_v<T, U>)
[[nodiscard]] friend constexpr auto operator+(safe_int<T, EP> lhs, safe_int<U, EP> rhs)
-> safe_int<integral_op_result_t<T, U>, EP>
{
using R = integral_op_result_t<T, U>;
if (add_overflows<R>(static_cast<R>(lhs.value_), static_cast<R>(rhs.value_)))
EP::on_overflow("safe_int: addition overflow");
return lhs.value_ + rhs.value_;
}
template<integral T, integral U, typename EP>
requires(!std::same_as<T, U> && same_sign_v<T, U>)
[[nodiscard]] friend constexpr auto operator-(safe_int<T, EP> lhs, safe_int<U, EP> rhs)
-> safe_int<integral_op_result_t<T, U>, EP>
{
using R = integral_op_result_t<T, U>;
if (sub_overflows<R>(static_cast<R>(lhs.value_), static_cast<R>(rhs.value_)))
EP::on_overflow("safe_int: subtraction overflow");
return lhs.value_ - rhs.value_;
}
template<integral T, integral U, typename EP>
requires(!std::same_as<T, U> && same_sign_v<T, U>)
[[nodiscard]] friend constexpr auto operator*(safe_int<T, EP> lhs, safe_int<U, EP> rhs)
-> safe_int<integral_op_result_t<T, U>, EP>
{
using R = integral_op_result_t<T, U>;
if (mul_overflows<R>(static_cast<R>(lhs.value_), static_cast<R>(rhs.value_)))
EP::on_overflow("safe_int: multiplication overflow");
return lhs.value_ * rhs.value_;
}
template<integral T, integral U, typename EP>
requires(!std::same_as<T, U> && same_sign_v<T, U>)
[[nodiscard]] friend constexpr auto operator/(safe_int<T, EP> lhs, safe_int<U, EP> rhs)
-> safe_int<integral_op_result_t<T, U>, EP>
{
using R = integral_op_result_t<T, U>;
if (div_overflows<R>(static_cast<R>(lhs.value_), static_cast<R>(rhs.value_)))
EP::on_overflow("safe_int: division overflow");
return lhs.value_ / rhs.value_;
}
template<integral T, integral U, typename EP>
requires(!std::same_as<T, U> && same_sign_v<T, U>)
[[nodiscard]] friend constexpr auto operator%(safe_int<T, EP> lhs, safe_int<U, EP> rhs)
-> safe_int<integral_op_result_t<T, U>, EP>
{
if (rhs.value_ == U{0}) EP::on_overflow("safe_int: modulo by zero");
return lhs.value_ % rhs.value_;
}
};
} // namespace detail
+41 -3
View File
@@ -617,6 +617,41 @@ static_assert(safe_int<int>{6} * 2LL == safe_int<long long>{12});
static_assert(2LL * safe_int<int>{6} == safe_int<long long>{12});
static_assert(safe_int<int>{12} / 4LL == safe_int<long long>{3});
// ============================================================================
// Cross-type integral — additional convertibility coverage
//
// Exercises the `safe_int<T> op integral_U` overloads for combinations not covered above:
// - narrower raw RHS (promotes to T's rank, stays safe_int<T>)
// - `long` (whose width differs across platforms — 32-bit on Windows, 64-bit on POSIX),
// verifying that the safe_int op integral overload is chosen over implicit conversion
// of the raw integer to safe_int<T> via the converting ctor.
// ============================================================================
// Narrowing raw RHS: result keeps the wider wrapper type.
// short → int by C++ promotion rules, so safe_int<int> + short → safe_int<int>.
static_assert(std::is_same_v<decltype(safe_int<int>{} + short{1}), safe_int<int>>);
static_assert(std::is_same_v<decltype(short{1} + safe_int<int>{}), safe_int<int>>);
static_assert(safe_int<int>{3} + short{4} == safe_int<int>{7});
static_assert(short{4} + safe_int<int>{3} == safe_int<int>{7});
// safe_int<short> + int → safe_int<int> (raw RHS has higher rank).
static_assert(std::is_same_v<decltype(safe_int<short>{} + 1), safe_int<int>>);
static_assert(std::is_same_v<decltype(1 + safe_int<short>{}), safe_int<int>>);
static_assert(safe_int<short>{3} + 4 == safe_int<int>{7});
// safe_int<int> + 1L: result is safe_int<long>. On Windows where `long` and `int` have
// equal width this would otherwise be tempted by the converting ctor (raw long → safe_int<int>),
// which would yield safe_int<int>; the safe_int op integral overload wins by exact match.
static_assert(std::is_same_v<decltype(safe_int<int>{} + 1L), safe_int<long>>);
static_assert(std::is_same_v<decltype(1L + safe_int<int>{}), safe_int<long>>);
static_assert(safe_int<int>{3} + 4L == safe_int<long>{7L});
static_assert(4L + safe_int<int>{3} == safe_int<long>{7L});
// Unsigned widening: safe_int<unsigned> + uint8_t → safe_int<unsigned>.
static_assert(std::is_same_v<decltype(safe_int<unsigned>{} + std::uint8_t{1}), safe_int<unsigned>>);
static_assert(std::is_same_v<decltype(std::uint8_t{1} + safe_int<unsigned>{}), safe_int<unsigned>>);
static_assert(safe_int<unsigned>{3u} + std::uint8_t{4} == safe_int<unsigned>{7u});
// int × unsigned scalar arithmetic is intentionally ill-formed — same rationale as
// safe_int<int> + safe_int<unsigned>: sign-mismatch conversions produce counterintuitive
// results (e.g. safe_int<int>{-1} * 2u → UINT_MAX-1 via reinterpretation).
@@ -763,9 +798,12 @@ static_assert(safe_int<int>{3} * safe_int<long>{4L} == safe_int<long>{12L});
static_assert(safe_int<int>{12} / safe_int<long>{4L} == safe_int<long>{3L});
static_assert(safe_int<int>{10} % safe_int<long>{3L} == safe_int<long>{1L});
// Widening prevents overflow: INT_MAX fits in long, so no overflow here
static_assert(safe_int<int>{std::numeric_limits<int>::max()} + safe_int<long>{1L} ==
safe_int<long>{static_cast<long>(std::numeric_limits<int>::max()) + 1L});
// Widening prevents overflow: INT_MAX + 1 fits in long long on every platform.
// (`long long` rather than `long`: on Windows `long` is 32-bit and equal to `int`, so the
// RHS `static_cast<long>(INT_MAX) + 1L` would itself be 32-bit signed overflow — UB at
// constexpr — and the assertion would not be portable.)
static_assert(safe_int<int>{std::numeric_limits<int>::max()} + safe_int<long long>{1LL} ==
safe_int<long long>{static_cast<long long>(std::numeric_limits<int>::max()) + 1LL});
// Heterogeneous comparison (operator== and operator<=>)
static_assert(safe_int<int>{3} == safe_int<long>{3L});