Skip to content

Commit ad77bcc

Browse files
committed
feat: safe_int::operator T made explicit
1 parent 9c884f2 commit ad77bcc

File tree

2 files changed

+115
-19
lines changed

2 files changed

+115
-19
lines changed

src/core/include/mp-units/safe_int.h

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -742,11 +742,25 @@ class safe_int : detail::safe_int_binary_ops {
742742
{
743743
}
744744

745-
// Implicit conversion to the underlying type allows passing safe_int to legacy
746-
// interfaces that accept raw integral types without an explicit .value() or cast.
747-
[[nodiscard]] constexpr explicit(false) operator T() const noexcept { return value_; }
745+
// Explicit conversion to the underlying type: use .value() or static_cast<T>() when
746+
// you need a raw integer. Keeping this explicit prevents safe_int from silently
747+
// decaying back to an unprotected T, preserves common_type resolution with raw
748+
// integer types (only the T→safe_int<T> direction is then implicit), and avoids
749+
// the ternary ambiguity that prevents std::common_type from working.
750+
[[nodiscard]] constexpr explicit operator T() const noexcept { return value_; }
748751
[[nodiscard]] constexpr T value() const noexcept { return value_; }
749752

753+
#if MP_UNITS_HOSTED
754+
template<typename CharT, typename Traits>
755+
friend std::basic_ostream<CharT, Traits>& operator<<(std::basic_ostream<CharT, Traits>& os, const safe_int& v)
756+
{
757+
if constexpr (sizeof(T) == 1)
758+
return os << static_cast<int>(v.value_); // promote char-width types to int for streaming
759+
else
760+
return os << v.value_;
761+
}
762+
#endif
763+
750764
// ==========================================================================
751765
// Unary operators (+, -, ++, --)
752766
// Models integral promotion: sub-int types (char, short) promote to int.

test/static/safe_int_test.cpp

Lines changed: 98 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,11 @@ static_assert(static_cast<int>(safe_int<int>{-7}) == -7);
240240
static_assert(safe_int<int>{0} == safe_int<int>{0});
241241
static_assert(safe_int<int>{1} != safe_int<int>{2});
242242

243+
// operator T() is explicit: safe_int does not silently decay to the raw integer type.
244+
// Use .value() or static_cast<T>() to extract the underlying value intentionally.
245+
static_assert(!std::is_convertible_v<safe_int<int>, int>);
246+
static_assert(std::is_constructible_v<int, safe_int<int>>); // explicit cast still works
247+
243248
// ============================================================================
244249
// Converting constructors — raw integer types
245250
// ============================================================================
@@ -314,41 +319,64 @@ static_assert(safe_int{100}.value() == 100);
314319
static_assert(safe_int{short{42}}.value() == 42);
315320

316321
// ============================================================================
317-
// common_type deduction (no explicit specialization needed — implicit widening resolves the ternary)
322+
// common_type deduction
323+
//
324+
// Because operator T() is explicit, the only implicit conversion paths are the
325+
// value-preserving constructors (widening). The ternary used by std::common_type
326+
// therefore has at most one viable implicit direction, so ternary resolution is
327+
// unambiguous and the wrapper is never silently dropped.
328+
//
329+
// Helper: uses a dependent requires-expression so that "no viable conversion"
330+
// (neither direction is implicit) becomes a clean SFINAE failure rather than a
331+
// hard error — the dependent T/U defer the check to substitution time.
318332
// ============================================================================
319333

334+
template<typename T, typename U>
335+
inline constexpr bool has_common_type = requires(T a, U b) { false ? a : b; };
336+
337+
// Widening between same-sign wrappers: one direction is implicit → resolves correctly.
320338
static_assert(
321339
std::is_same_v<std::common_type_t<safe_int<short>, safe_int<int>>, safe_int<int, safe_int<short>::error_policy>>);
322340
static_assert(std::is_same_v<std::common_type_t<safe_int<std::int8_t>, safe_int<int>>,
323341
safe_int<int, safe_int<std::int8_t>::error_policy>>);
324342
static_assert(std::is_same_v<std::common_type_t<safe_int<std::uint8_t>, safe_int<unsigned>>,
325343
safe_int<unsigned, safe_int<std::uint8_t>::error_policy>>);
326344

327-
// safe_int vs raw integer: wrapper drops off (safe_int<short> → short → int via operator T())
328-
static_assert(std::is_same_v<std::common_type_t<safe_int<short>, int>, int>);
329-
static_assert(std::is_same_v<std::common_type_t<int, safe_int<short>>, int>);
345+
// safe_int<int> vs raw int: with explicit operator T(), only int→safe_int<int> is
346+
// implicit (value-preserving ctor). The wrapper is preserved, not dropped.
347+
static_assert(std::is_same_v<std::common_type_t<safe_int<int>, int>, safe_int<int>>);
348+
static_assert(std::is_same_v<std::common_type_t<int, safe_int<int>>, safe_int<int>>);
349+
350+
// safe_int<short> vs raw int: int→safe_int<short> is explicit (narrowing), and
351+
// safe_int<short>→int requires explicit operator T(). Neither direction is implicit
352+
// → no common_type. This prevents silently mixing safe and unsafe integer types.
353+
static_assert(!has_common_type<safe_int<short>, int>);
354+
static_assert(!has_common_type<int, safe_int<short>>);
330355

331-
// Widening unsigned→signed: uint8_t fits in int, so converting ctor is implicit
356+
// Widening unsigned→signed: uint8_t fits in int, so converting ctor is implicit.
332357
static_assert(std::is_same_v<std::common_type_t<safe_int<std::uint8_t>, safe_int<int>>,
333358
safe_int<int, safe_int<std::uint8_t>::error_policy>>);
334359

335-
// Signed↔unsigned same size: neither converting ctor is implicit (both directions
336-
// are not value-preserving), so the wrapper drops off and common_type resolves to
337-
// the underlying common_type (unsigned, via standard integral promotion).
338-
static_assert(std::is_same_v<std::common_type_t<safe_int<int>, safe_int<unsigned>>, unsigned>);
339-
static_assert(std::is_same_v<std::common_type_t<safe_int<unsigned>, safe_int<int>>, unsigned>);
360+
// Signed↔unsigned same size: both constructors are explicit (ranges don't overlap)
361+
// and operator T() is explicit → no common_type. Mixed-signedness arithmetic
362+
// must be resolved explicitly by the user.
363+
static_assert(!has_common_type<safe_int<int>, safe_int<unsigned>>);
364+
static_assert(!has_common_type<safe_int<unsigned>, safe_int<int>>);
340365

341-
// Cross-wrapper: safe_int<T> vs constrained<T, P> — safe_int has an implicit converting
342-
// constructor from constrained<T> (value-preserving when ranges match), so common_type
343-
// resolves to safe_int.
366+
// Cross-wrapper: safe_int<T> has an implicit constructor from constrained<T, P>
367+
// (value-preserving), while constrained<T> only constructs from T directly (would
368+
// need two UDCs via safe_int's operator T() — not allowed). So only one direction
369+
// is implicit → common_type resolves to safe_int.
344370
static_assert(std::is_same_v<std::common_type_t<safe_int<int>, constrained<int, test_policy>>, safe_int<int>>);
345371
static_assert(std::is_same_v<std::common_type_t<constrained<int, test_policy>, safe_int<int>>, safe_int<int>>);
346372

347-
// Cross-wrapper with widening: constrained<short> → safe_int<int> is implicit (range fits)
373+
// Cross-wrapper with widening: constrained<short> → safe_int<int> is implicit (range fits).
348374
static_assert(std::is_same_v<std::common_type_t<constrained<short, test_policy>, safe_int<int>>, safe_int<int>>);
349375

350-
// Cross-wrapper where neither direction is implicit: wrapper drops off
351-
static_assert(std::is_same_v<std::common_type_t<safe_int<short>, constrained<int, test_policy>>, int>);
376+
// Cross-wrapper where safe_int<short>→constrained<int> is not implicit (would need
377+
// operator T() to go short→int then int→constrained<int>: two UDCs), and the
378+
// reverse is explicit (narrowing) → no common_type.
379+
static_assert(!has_common_type<safe_int<short>, constrained<int, test_policy>>);
352380

353381
// ============================================================================
354382
// Comparison operators
@@ -897,4 +925,58 @@ static_assert([] {
897925
return dist.numerical_value_in(m) == 100;
898926
}());
899927

928+
// ============================================================================
929+
// Quantity-level common_type and implicit conversion with safe_int reps
930+
//
931+
// The framework's quantity common_type is:
932+
// quantity<get_common_reference(R1,R2), common_type_t<Rep1,Rep2>>
933+
// It uses common_type_t<Rep1,Rep2> directly (not the ternary trick on quantities).
934+
// This means bidirectional quantity-level implicit constructors (implicitly_scalable
935+
// is true for same-unit non-FP) do NOT cause ambiguous ternaries — the raw
936+
// safe_int level (where only widening is implicit) resolves common_type correctly.
937+
// ============================================================================
938+
939+
using namespace mp_units::si::unit_symbols;
940+
using qty_m_int = quantity<isq::length[si::metre], int>;
941+
using qty_m_si = quantity<isq::length[si::metre], safe_int<int>>;
942+
943+
// Widening: safe_int<short> → safe_int<int> is implicit at both rep and quantity level.
944+
static_assert(std::is_convertible_v<quantity<isq::length[si::metre], safe_int<short>>,
945+
quantity<isq::length[si::metre], safe_int<int>>>);
946+
947+
// Narrowing: safe_int<int> → safe_int<short> is explicit at both the rep and quantity
948+
// level: the quantity constructor gates on `std::convertible_to<Rep2, rep>`, which is
949+
// false when the rep's own constructor is explicit.
950+
static_assert(!std::is_convertible_v<quantity<isq::length[si::metre], safe_int<int>>,
951+
quantity<isq::length[si::metre], safe_int<short>>>);
952+
static_assert(std::is_constructible_v<quantity<isq::length[si::metre], safe_int<short>>,
953+
quantity<isq::length[si::metre], safe_int<int>>>);
954+
955+
// Common type: quantity<m, safe_int<int>> wins because common_type_t<safe_int<int>, safe_int<short>>
956+
// = safe_int<int> (widening at the raw safe_int level is one-directional: short→int implicit only).
957+
static_assert(std::is_same_v<std::common_type_t<quantity<isq::length[si::metre], safe_int<int>>,
958+
quantity<isq::length[si::metre], safe_int<short>>>,
959+
quantity<isq::length[si::metre], safe_int<int>>>);
960+
961+
static_assert(std::is_same_v<std::common_type_t<quantity<isq::length[si::metre], safe_int<short>>,
962+
quantity<isq::length[si::metre], safe_int<int>>>,
963+
quantity<isq::length[si::metre], safe_int<int>>>);
964+
965+
// With explicit operator T(), common_type<safe_int<int>, int> = safe_int<int>, so
966+
// mixing quantity<m, safe_int<int>> with quantity<m, int> is now well-formed and
967+
// the result preserves the safety wrapper.
968+
static_assert(std::is_same_v<std::common_type_t<quantity<isq::length[si::metre], safe_int<int>>,
969+
quantity<isq::length[si::metre], int>>,
970+
quantity<isq::length[si::metre], safe_int<int>>>);
971+
972+
973+
974+
// unit-conversion implicit: m→mm scales by ×1000 (integral), so implicitly_scalable=true.
975+
static_assert(std::is_convertible_v<quantity<isq::length[si::metre], safe_int<int>>,
976+
quantity<isq::length[si::milli<si::metre>], safe_int<int>>>);
977+
978+
// unit-conversion explicit: mm→m scales by ÷1000 (non-integral), so implicitly_scalable=false.
979+
static_assert(!std::is_convertible_v<quantity<isq::length[si::milli<si::metre>], safe_int<int>>,
980+
quantity<isq::length[si::metre], safe_int<int>>>);
981+
900982
} // namespace

0 commit comments

Comments
 (0)