@@ -240,6 +240,11 @@ static_assert(static_cast<int>(safe_int<int>{-7}) == -7);
240240static_assert (safe_int<int >{0 } == safe_int<int >{0 });
241241static_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);
314319static_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.
320338static_assert (
321339 std::is_same_v<std::common_type_t <safe_int<short >, safe_int<int >>, safe_int<int , safe_int<short >::error_policy>>);
322340static_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>>);
324342static_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.
332357static_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.
344370static_assert (std::is_same_v<std::common_type_t <safe_int<int >, constrained<int , test_policy>>, safe_int<int >>);
345371static_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).
348374static_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