From ea1e0c16efa97c181ba227540ec673593a5f8c8c Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:33:40 +0900 Subject: [PATCH 01/14] Rewrite unit related logic in common form --- include/iris/type_traits.hpp | 69 +++++++ include/iris/units/concepts.hpp | 77 ++++++++ include/iris/units/quantity.hpp | 328 ++++++++++++++------------------ include/iris/units/traits.hpp | 192 +++++++++++++++++++ test/CMakeLists.txt | 1 + test/type_traits.cpp | 146 ++++++++++++++ test/units/concepts.cpp | 42 ++++ test/units/quantity.cpp | 32 ++-- 8 files changed, 689 insertions(+), 198 deletions(-) create mode 100644 include/iris/units/concepts.hpp create mode 100644 include/iris/units/traits.hpp create mode 100644 test/units/concepts.cpp diff --git a/include/iris/type_traits.hpp b/include/iris/type_traits.hpp index 016c670..1215112 100644 --- a/include/iris/type_traits.hpp +++ b/include/iris/type_traits.hpp @@ -76,6 +76,75 @@ struct remove_cv using apply = F const volatile; }; +// ----------------------------------------------------------- + +namespace detail { + +template +concept dominant_type_candidate_dominates = + requires { typename std::common_type_t>; } && + std::same_as>, std::decay_t>; + +template +struct dominant_type_candidate + : std::bool_constant<(dominant_type_candidate_dominates && ...)> +{ + using type = std::decay_t; +}; + +} // detail + +// The type among `Ts...` that dominates all the others, i.e., the `Cand` in `Ts...` +// such that `std::common_type_t` is `Cand` for every `T`. +// +// The result is always one of `Ts...` and does not depend on their order; no `type` +// member if no such type exists (e.g. `short` and `char`, whose common type `int` is +// not among them). +template +struct dominant_type +{ + // No `::type` +}; +template +using dominant_type_t = dominant_type::type; + +template + requires std::disjunction...>::value +struct dominant_type + : std::disjunction...> +{}; + +// `T` dominates every type in `Ts...`, i.e., `std::common_type_t` is `T` for +// every `U` in `Ts...`. +// +// Can be used to constrain the explicitness of conversion or assignment. +template +concept dominant = (detail::dominant_type_candidate_dominates && ...); + +// Denotes `iris::dominant_type` if it exists, otherwise equivalent to `std::common_type`. +// +// `std::common_type_t` +// -> `float` +// `std::common_type_t` +// -> ill-formed +// `iris::symmetric_common_type_t` +// -> `float` (in any order) +// +// Use `iris::dominant_type` instead when the result must be one of `Ts...`. +template +struct symmetric_common_type : std::common_type +{}; +template +using symmetric_common_type_t = symmetric_common_type::type; + +template + requires requires { + typename dominant_type::type; + } +struct symmetric_common_type : dominant_type +{}; + +// ----------------------------------------------------------- template struct type_list diff --git a/include/iris/units/concepts.hpp b/include/iris/units/concepts.hpp new file mode 100644 index 0000000..aae48b9 --- /dev/null +++ b/include/iris/units/concepts.hpp @@ -0,0 +1,77 @@ +#ifndef IRIS_ZZ_UNITS_CONCEPTS_HPP +#define IRIS_ZZ_UNITS_CONCEPTS_HPP + +// SPDX-License-Identifier: MIT + +#include // IWYU pragma: keep + +#include + +#include +#include +#include + +namespace iris::units { + +template +using scalar_t = std::remove_cvref_t() / std::declval())>; + +template +concept scalar = iris::numeric_arithmetic; + +// `T` models `ordered_linear` if `T` is a totally ordered one-dimensional +// vector space over `scalar_t`. +// +// For example, a class derived from `iris::units::quantity` models +// `ordered_linear` when `Rep` is a floating-point type. Integral `Rep` +// does not, because integer division is not the inverse of multiplication. +// +// Preconditions: +// Values of `T` (and of `scalar_t`) passed to algorithms constrained by +// this concept satisfy the following conditions: +// - They are not NaN. +// - They are finite. In particular, infinity must not be used as a sentinel +// for an unbounded or invalid value. +// - Arithmetic among them does not overflow to infinity. +// +// Semantic requirements are as follows: +// - Let `a`, `b`, and `c` denote instances of `T`. +// - Let `s` denote an instance of `scalar_t`. +// - `T{}` is the additive identity: `a + T{} == a`. +// - `a <= b` implies `a + c <= b + c`. +// - `T{} <= a` and `0 <= s` imply `T{} <= a * s`. +// - For `b != T{}`, `(a / b) * b` approximates `a` within floating-point rounding. +template +concept ordered_linear = + std::regular && + std::totally_ordered && + requires { + typename scalar_t; + } && + requires(T const& t, scalar_t const& s) { + { t + t } -> std::same_as; + { t - t } -> std::same_as; + { t * s } -> std::same_as; + { t / s } -> std::same_as; + { t / t } -> scalar; + } && + std::floating_point> /* scalars must form a field */; + +template +concept nothrow_ordered_linear = + ordered_linear && + std::is_nothrow_default_constructible_v && + std::is_nothrow_copy_constructible_v && + std::is_nothrow_copy_assignable_v && + requires(T const& t, scalar_t const& s) { + { t + t } noexcept; + { t - t } noexcept; + { t * s } noexcept; + { t / s } noexcept; + { t / t } noexcept; + { t <= t } noexcept; + }; + +} // iris::units + +#endif diff --git a/include/iris/units/quantity.hpp b/include/iris/units/quantity.hpp index 7f890b5..fa2c126 100644 --- a/include/iris/units/quantity.hpp +++ b/include/iris/units/quantity.hpp @@ -5,6 +5,8 @@ #include // IWYU pragma: keep +#include + #include #include @@ -25,43 +27,28 @@ namespace iris::units { template class quantity; -template -struct quantity_traits; - template class DerivedTT, numeric_arithmetic T, class... Rest> requires std::derived_from, quantity> && (!std::same_as, quantity>) -struct quantity_traits> +struct unit_traits> { using value_type = T; + template + using base_rebind = quantity; + template using rebind = DerivedTT; }; template -concept quantity_like = - requires { typename quantity_traits::value_type; } && - std::derived_from::value_type>>; - -namespace detail { - -template -concept same_quantity_family = - quantity_like && - (std::same_as< - typename quantity_traits::template rebind::value_type>, - Rest - > && ...); +concept quantity_class = unit_class_of; -template -using common_quantity_value_t = std::common_type_t< - typename quantity_traits::value_type, - typename quantity_traits::value_type ->; +template +concept quantity_family = unit_family_of; -} // detail +// ------------------------------------------------- // Declares the deduction guide for a quantity family, so that `DerivedClass{value}` // deduces `DerivedClass`. @@ -124,77 +111,80 @@ class quantity } template - requires detail::same_quantity_family && (!std::same_as) + requires (!std::same_as) && quantity_family [[nodiscard]] constexpr - explicit(!std::same_as< - detail::common_quantity_value_t, - typename quantity_traits::value_type - >) + explicit(!dominant_unit) operator Target(this Self const& self) noexcept { - return Target{static_cast::value_type>(self.value)}; + return Target{static_cast>(self.value)}; } // ---------------------------------------------------- template - requires detail::same_quantity_family + requires quantity_family [[nodiscard]] constexpr bool operator==(this Self const& self, Other const& other) noexcept { - using common = detail::common_quantity_value_t; + using common = common_value_type_t; return static_cast(self.value) == static_cast(other.value); } template - requires detail::same_quantity_family + requires quantity_family [[nodiscard]] constexpr auto operator<=>(this Self const& self, Other const& other) noexcept { - using common = detail::common_quantity_value_t; + using common = common_value_type_t; return static_cast(self.value) <=> static_cast(other.value); } // ---------------------------------------------------- template - [[nodiscard]] constexpr Self operator+(this Self const& self) noexcept + [[nodiscard]] constexpr Self + operator+(this Self const& self) noexcept { return self; } template - [[nodiscard]] constexpr Self operator-(this Self const& self) noexcept + [[nodiscard]] constexpr Self + operator-(this Self const& self) noexcept { - return Self{static_cast::value_type>(-self.value)}; + return Self{static_cast>(-self.value)}; } template - requires std::integral::value_type> - constexpr Self& operator++(this Self& self) noexcept + requires std::integral> + constexpr Self& + operator++(this Self& self) noexcept { ++self.value; return self; } template - requires std::integral::value_type> - constexpr Self operator++(this Self& self, int) noexcept + requires std::integral> + constexpr Self + operator++(this Self& self, int) noexcept { return Self{self.value++}; } template - requires std::integral::value_type> - constexpr Self& operator--(this Self& self) noexcept + requires std::integral> + constexpr Self& + operator--(this Self& self) noexcept { --self.value; return self; } template - requires std::integral::value_type> - constexpr Self operator--(this Self& self, int) noexcept + requires std::integral> + constexpr Self + operator--(this Self& self, int) noexcept { return Self{self.value--}; } @@ -202,36 +192,36 @@ class quantity // ---------------------------------------------------- template - requires detail::same_quantity_family + requires quantity_family [[nodiscard]] constexpr std::common_type_t operator+(this Self const& self, Other const& other) noexcept { using result = std::common_type_t; - return result{static_cast::value_type>(self.value + other.value)}; + return result{static_cast>(self.value + other.value)}; } template - requires detail::same_quantity_family && - std::same_as, typename quantity_traits::value_type> - constexpr Self& operator+=(this Self& self, Other const& other) noexcept + requires dominant_unit + constexpr Self& + operator+=(this Self& self, Other const& other) noexcept { self.value += other.value; return self; } template - requires detail::same_quantity_family + requires quantity_family [[nodiscard]] constexpr std::common_type_t operator-(this Self const& self, Other const& other) noexcept { using result = std::common_type_t; - return result{static_cast::value_type>(self.value - other.value)}; + return result{static_cast>(self.value - other.value)}; } template - requires detail::same_quantity_family && - std::same_as, typename quantity_traits::value_type> - constexpr Self& operator-=(this Self& self, Other const& other) noexcept + requires dominant_unit + constexpr Self& + operator-=(this Self& self, Other const& other) noexcept { self.value -= other.value; return self; @@ -240,16 +230,18 @@ class quantity // ---------------------------------------------------- template - requires std::same_as::value_type, U>, typename quantity_traits::value_type> - constexpr Self& operator*=(this Self& self, U scalar) noexcept + requires dominant, U> + constexpr Self& + operator*=(this Self& self, U scalar) noexcept { self.value *= scalar; return self; } template - requires std::same_as::value_type, U>, typename quantity_traits::value_type> - constexpr Self& operator/=(this Self& self, U scalar) noexcept + requires dominant, U> + constexpr Self& + operator/=(this Self& self, U scalar) noexcept { self.value /= scalar; return self; @@ -258,55 +250,60 @@ class quantity // ---------------------------------------------------- template - [[nodiscard]] constexpr auto operator*(this Self const& self, U scalar) noexcept - -> quantity_traits::template rebind::value_type, U>> + [[nodiscard]] constexpr auto + operator*(this Self const& self, U scalar) noexcept + -> detail::rebind_t, U>> { - using common = std::common_type_t::value_type, U>; - return typename quantity_traits::template rebind{static_cast(self.value * scalar)}; + using common = std::common_type_t, U>; + return detail::rebind_t{static_cast(self.value * scalar)}; } // Scalar on the left (cannot use explicit object parameter) - template - requires std::derived_from - [[nodiscard]] friend constexpr auto operator*(U scalar, Q const& q) noexcept - -> quantity_traits::template rebind::value_type, U>> + template + requires std::derived_from // depend on self type to avoid ODR + [[nodiscard]] friend constexpr auto + operator*(U scalar, Q const& q) noexcept + -> detail::rebind_t, U>> { - using common = std::common_type_t::value_type, U>; - return typename quantity_traits::template rebind{static_cast(scalar * q.value)}; + using common = std::common_type_t, U>; + return detail::rebind_t{static_cast(scalar * q.value)}; } - template + template void operator*(this Self const&, Other const&) = delete; // ---------------------------------------------------- template - [[nodiscard]] constexpr auto operator/(this Self const& self, U scalar) noexcept - -> quantity_traits::template rebind::value_type, U>> + [[nodiscard]] constexpr auto + operator/(this Self const& self, U scalar) noexcept + -> detail::rebind_t, U>> { - using common = std::common_type_t::value_type, U>; - return typename quantity_traits::template rebind{static_cast(self.value / scalar)}; + using common = std::common_type_t, U>; + return detail::rebind_t{static_cast(self.value / scalar)}; } template - requires detail::same_quantity_family - [[nodiscard]] constexpr detail::common_quantity_value_t + requires quantity_family + [[nodiscard]] constexpr common_value_type_t operator/(this Self const& self, Other const& other) noexcept { - return static_cast>(self.value / other.value); + return static_cast>(self.value / other.value); } - template - requires std::derived_from + template + requires std::derived_from // depend on self type to avoid ODR friend void operator/(U, Q const&) = delete; // ---------------------------------------------------- template requires - std::integral::value_type> && std::integral && - std::same_as::value_type, U>, typename quantity_traits::value_type> - constexpr Self& operator%=(this Self& self, U scalar) noexcept + std::integral> && + std::integral && + dominant, U> + constexpr Self& + operator%=(this Self& self, U scalar) noexcept { self.value %= scalar; return self; @@ -314,80 +311,47 @@ class quantity template requires - detail::same_quantity_family && - std::integral::value_type> && - std::integral::value_type> && - std::same_as, typename quantity_traits::value_type> - constexpr Self& operator%=(this Self& self, Other const& other) noexcept + std::integral> && + std::integral> && + dominant_unit + constexpr Self& + operator%=(this Self& self, Other const& other) noexcept { self.value %= other.value; return self; } template - requires std::integral::value_type> && std::integral - [[nodiscard]] constexpr auto operator%(this Self const& self, U scalar) noexcept - -> quantity_traits::template rebind::value_type, U>> + requires std::integral> && std::integral + [[nodiscard]] constexpr auto + operator%(this Self const& self, U scalar) noexcept + -> detail::rebind_t, U>> { - using common = std::common_type_t::value_type, U>; - return typename quantity_traits::template rebind{static_cast(self.value % scalar)}; + using common = std::common_type_t, U>; + return detail::rebind_t{static_cast(self.value % scalar)}; } template requires - detail::same_quantity_family && - std::integral::value_type> && - std::integral::value_type> + std::integral> && + std::integral> && + quantity_family [[nodiscard]] constexpr std::common_type_t operator%(this Self const& self, Other const& other) noexcept { using result = std::common_type_t; - return result{static_cast::value_type>(self.value % other.value)}; + return result{static_cast>(self.value % other.value)}; } - template - requires std::derived_from + template + requires std::derived_from // depend on self type to avoid ODR friend void operator%(U, Q const&) = delete; }; } // iris::units -template< - template class DerivedTT, - iris::numeric_arithmetic T, iris::numeric_arithmetic U, class... Rest -> - requires - iris::units::quantity_like> && - iris::units::quantity_like> -struct std::common_type, DerivedTT> -{ - using type = DerivedTT, Rest...>; -}; - -// A plain quantity and a derived quantity have no common type -template - requires - iris::units::quantity_like && - std::derived_from> && - (!std::same_as>) -struct std::common_type, Derived> -{ - // No `::type` -}; - -// A plain quantity and a derived quantity have no common type -template - requires - iris::units::quantity_like && - std::derived_from> && - (!std::same_as>) -struct std::common_type> -{ - // No `::type` -}; - template class DerivedTT, iris::numeric_arithmetic T, class... Rest> - requires iris::units::quantity_like> + requires iris::units::quantity_class> class std::numeric_limits> : public std::numeric_limits { public: @@ -431,69 +395,69 @@ class std::numeric_limits> : public std::numeric_limits namespace iris::units { -template - requires std::floating_point::value_type> +template + requires std::floating_point> [[nodiscard]] constexpr Q trunc(Q const& q) noexcept { return Q{std::trunc(q.value)}; } -template - requires std::integral::value_type> +template + requires std::integral> [[nodiscard]] constexpr Q trunc(Q const& q) noexcept { return q; } -template - requires std::floating_point::value_type> +template + requires std::floating_point> [[nodiscard]] constexpr Q floor(Q const& q) noexcept { return Q{std::floor(q.value)}; } -template - requires std::integral::value_type> +template + requires std::integral> [[nodiscard]] constexpr Q floor(Q const& q) noexcept { return q; } -template - requires std::floating_point::value_type> +template + requires std::floating_point> [[nodiscard]] constexpr Q ceil(Q const& q) noexcept { return Q{std::ceil(q.value)}; } -template - requires std::integral::value_type> +template + requires std::integral> [[nodiscard]] constexpr Q ceil(Q const& q) noexcept { return q; } -template - requires std::floating_point::value_type> +template + requires std::floating_point> [[nodiscard]] constexpr Q round(Q const& q) noexcept { return Q{std::round(q.value)}; } -template - requires std::integral::value_type> +template + requires std::integral> [[nodiscard]] constexpr Q round(Q const& q) noexcept { return q; } -template +template [[nodiscard]] constexpr Q abs(Q const& q) noexcept { - if constexpr (std::unsigned_integral::value_type>) { + if constexpr (std::unsigned_integral>) { return q; } else { - return Q{static_cast::value_type>(std::abs(q.value))}; + return Q{static_cast>(std::abs(q.value))}; } } @@ -502,8 +466,8 @@ template // Returns by value, unlike `std::min`, since the result is in the common // representation and may not be any of the arguments. `std::min` remains // available when a reference is wanted, but only for a single representation. -template - requires detail::same_quantity_family +template + requires quantity_family [[nodiscard]] constexpr std::common_type_t (min)(A const& a, B const& b) noexcept { @@ -514,9 +478,9 @@ template // Returns by value, unlike `std::min`, since the result is in the common // representation and may not be any of the arguments. `std::min` remains // available when a reference is wanted, but only for a single representation. -template +template requires - detail::same_quantity_family && + quantity_family && std::strict_weak_order const&, std::common_type_t const&> [[nodiscard]] constexpr std::common_type_t (min)(A const& a, B const& b, Comp&& comp) @@ -529,8 +493,8 @@ template // Returns by value, unlike `std::max`, since the result is in the common // representation and may not be any of the arguments. `std::max` remains // available when a reference is wanted, but only for a single representation. -template - requires detail::same_quantity_family +template + requires quantity_family [[nodiscard]] constexpr std::common_type_t (max)(A const& a, B const& b) noexcept { @@ -541,9 +505,9 @@ template // Returns by value, unlike `std::max`, since the result is in the common // representation and may not be any of the arguments. `std::max` remains // available when a reference is wanted, but only for a single representation. -template +template requires - detail::same_quantity_family && + quantity_family && std::strict_weak_order const&, std::common_type_t const&> [[nodiscard]] constexpr std::common_type_t (max)(A const& a, B const& b, Comp&& comp) @@ -556,71 +520,71 @@ template // Returns by value, unlike `std::clamp`, since the result is in the common // representation and may not be any of the arguments. `std::clamp` remains // available when a reference is wanted, but only for a single representation. -template - requires detail::same_quantity_family -[[nodiscard]] constexpr std::common_type_t -clamp(Q const& v, Lo const& lo, Hi const& hi) +template + requires quantity_family +[[nodiscard]] constexpr common_unit_t +clamp(Q const& v, Lo const& lo, Hi const& hi) noexcept { - using result = std::common_type_t; + using result = common_unit_t; return std::clamp(static_cast(v), static_cast(lo), static_cast(hi)); } // Returns by value, unlike `std::clamp`, since the result is in the common // representation and may not be any of the arguments. `std::clamp` remains // available when a reference is wanted, but only for a single representation. -template +template requires - detail::same_quantity_family && - std::strict_weak_order const&, std::common_type_t const&> -[[nodiscard]] constexpr std::common_type_t + quantity_family && + std::strict_weak_order const&, common_unit_t const&> +[[nodiscard]] constexpr common_unit_t clamp(Q const& v, Lo const& lo, Hi const& hi, Comp&& comp) - noexcept(std::is_nothrow_invocable_v const&, std::common_type_t const&>) + noexcept(std::is_nothrow_invocable_v const&, common_unit_t const&>) { - using result = std::common_type_t; + using result = common_unit_t; return std::clamp(static_cast(v), static_cast(lo), static_cast(hi), std::forward(comp)); } // --------------------------------------------------- -template - requires detail::same_quantity_family +template + requires quantity_family [[nodiscard]] constexpr std::common_type_t midpoint(A a, B b) noexcept { using result = std::common_type_t; - using common = quantity_traits::value_type; + using common = detail::value_type_t; return result{std::midpoint(static_cast(a.value), static_cast(b.value))}; } -template +template requires - detail::same_quantity_family && - std::floating_point>::value_type> + quantity_family && + std::floating_point>> [[nodiscard]] constexpr std::common_type_t lerp(A a, B b, T t) noexcept { using result = std::common_type_t; - using common = quantity_traits::value_type; + using common = detail::value_type_t; return result{std::lerp(static_cast(a.value), static_cast(b.value), static_cast(t))}; } } // iris::units -template +template struct std::hash { [[nodiscard]] static std::size_t operator()(Q const& q) noexcept { - return std::hash::value_type>{}(q.value); + return std::hash>{}(q.value); } }; -template -struct std::formatter : std::formatter::value_type, CharT> +template +struct std::formatter : std::formatter, CharT> { auto format(Q const& q, auto& ctx) const { - return std::formatter::value_type, CharT>::format(q.value, ctx); + return std::formatter, CharT>::format(q.value, ctx); } }; diff --git a/include/iris/units/traits.hpp b/include/iris/units/traits.hpp new file mode 100644 index 0000000..8d947f7 --- /dev/null +++ b/include/iris/units/traits.hpp @@ -0,0 +1,192 @@ +#ifndef IRIS_ZZ_UNITS_TRAITS_HPP +#define IRIS_ZZ_UNITS_TRAITS_HPP + +// SPDX-License-Identifier: MIT + +#include // IWYU pragma: keep + +// Don't include ; dependency ordering is important + +#include + +#include +#include + +namespace iris::units { + +template +concept compatible_value_type = + requires { + typename symmetric_common_type_t; + }; + +template +struct unit_traits; + +namespace detail { + +template +using value_type_t = unit_traits>::value_type; + +template +using base_rebind_t = unit_traits>::template base_rebind; + +template +using rebind_t = unit_traits>::template rebind; + +template +concept unit_class_impl = + requires { + typename value_type_t; + typename base_rebind_t>; + typename rebind_t>; + } && + std::same_as< + Derived, + rebind_t> + > && + std::derived_from< + Derived, + base_rebind_t> + > && + // Exclude the case when `Derived` is not actually a derived class, i.e. it is the base class + !std::same_as< + Derived, + base_rebind_t> + >; + +template class UnitTT> +concept unit_class_of_impl = + unit_class_impl && + std::same_as< + base_rebind_t>, + UnitTT> + >; + +template +concept same_unit_class_impl = + unit_class_impl && + unit_class_impl && + std::same_as< + rebind_t>, + DerivedB + > && + std::same_as< + rebind_t>, + DerivedA + >; + +} // detail + +// `Derived = DerivedTT` is a unit class, i.e., a class +// derived from some arbitrary base unit class `BaseTT`. +template +concept unit_class = detail::unit_class_impl>; + +// `Derived = DerivedTT` is a unit class derived from `UnitTT`. +template class UnitTT> +concept unit_class_of = detail::unit_class_of_impl, UnitTT>; + +// `DerivedA` and `DerivedB` are specializations of the same unit class template +// (`DerivedTT` and `DerivedTT`), and their value types `T` and `U` +// are compatible. +template +concept unit_family_with = + detail::same_unit_class_impl, std::remove_cvref_t> && + compatible_value_type, detail::value_type_t>; + +// All of `First, Rest...` are specializations of the same unit class template, and +// their value types are pairwise compatible with that of `First`. +template +concept unit_family = + unit_class && + (unit_family_with && ...); + +// `unit_family` whose base unit class is `UnitTT`. +template class UnitTT, class First, class... Rest> +concept unit_family_of = + unit_family && + unit_class_of; + +} // iris::units + +// Specializations of the same unit class template share a common type whose value +// type is the common type of theirs +template< + template class DerivedTT, + class T, class U, class... Rest +> + requires iris::units::unit_family_with, DerivedTT> +struct std::common_type, DerivedTT> +{ + using type = DerivedTT, Rest...>; +}; + +// A base unit and a derived unit have no common type +template class DerivedTT, class T, class... Rest> + requires iris::units::unit_class> +struct std::common_type< + DerivedTT, + iris::units::detail::base_rebind_t< + DerivedTT, + iris::units::detail::value_type_t> + > +> +{ + // No `::type` +}; + +// A base unit and a derived unit have no common type +template class DerivedTT, class T, class... Rest> + requires iris::units::unit_class> +struct std::common_type< + iris::units::detail::base_rebind_t< + DerivedTT, + iris::units::detail::value_type_t> + >, + DerivedTT +> +{ + // No `::type` +}; + +// ------------------------------------------------------------------------- + +namespace iris::units { + +// The value type shared by the units in `Units...`, i.e., `symmetric_common_type_t` +// of their value types. +template + requires + unit_family && + compatible_value_type...> +using common_value_type_t = symmetric_common_type_t...>; + +// The unit type of `First` rebound to the common value type of `First, Rest...`. +template + requires unit_family +using common_unit_t = detail::rebind_t>; + +// `U` is the dominant unit among `U, Us...`, i.e., all are of the same unit family and +// the value type of `U` dominates those of `Us...` (in the sense of `dominant`). +// Converting from any of `Us...` to `U` is what the usual arithmetic conversions do, +// so such a conversion is implicit while the reverse is explicit. +template +concept dominant_unit = + unit_family && + dominant, detail::value_type_t...>; + +} // iris::units + +// ------------------------------------------------------------------------- + +namespace iris::units::detail { + +// Tag for constructors whose invariants are guaranteed by the caller; the runtime +// check is replaced by `assert`. +struct invariants_always_satisfied_t { constexpr explicit invariants_always_satisfied_t() = default; }; +inline constexpr invariants_always_satisfied_t invariants_always_satisfied{}; + +} // iris::units::detail + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7e2c38c..6adb446 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -209,6 +209,7 @@ if(PROJECT_IS_TOP_LEVEL) set( IRIS_TEST_UNITS_TESTS + concepts quantity ) foreach(test_name IN LISTS IRIS_TEST_UNITS_TESTS) diff --git a/test/type_traits.cpp b/test/type_traits.cpp index c30cc89..4646997 100644 --- a/test/type_traits.cpp +++ b/test/type_traits.cpp @@ -4,6 +4,13 @@ #include +#include + +#include +#include +#include +#include + #include template @@ -59,6 +66,145 @@ TEST_CASE("type traits") STATIC_CHECK(!iris::unsigned_numeric_integral); } +// https://en.cppreference.com/cpp/types/floating-point + +struct Float16 { explicit operator float() const noexcept { return 0.0f; } }; +struct BFloat16 { explicit operator float() const noexcept { return 0.0f; } }; + +template +concept StandardFloatingPoint = + std::same_as || std::same_as || std::same_as; + +// NOLINTBEGIN(bugprone-std-namespace-modification) +template struct std::common_type { using type = F; }; +template struct std::common_type { using type = F; }; +template struct std::common_type { using type = Float16; }; +template struct std::common_type { using type = Float16; }; + +template struct std::common_type { using type = F; }; +template struct std::common_type { using type = F; }; +template struct std::common_type { using type = BFloat16; }; +template struct std::common_type { using type = BFloat16; }; +// NOLINTEND(bugprone-std-namespace-modification) + +template +concept has_common_type = requires { typename std::common_type_t; }; + +template +concept has_dominant_type = requires { typename iris::dominant_type_t; }; + +template +concept has_symmetric_common_type = requires { typename iris::symmetric_common_type_t; }; + +#if __STDCPP_FLOAT16_T__ && __STDCPP_BFLOAT16_T__ +#define IRIS_TEST_NARROW_FP_PAIRS \ + (std::pair), (std::pair) +#else +#define IRIS_TEST_NARROW_FP_PAIRS \ + (std::pair) +#endif + +TEST_CASE("dominant_type: basic", "[type_traits]") +{ + STATIC_CHECK(!has_dominant_type<>); + + STATIC_CHECK(std::same_as, short>); + STATIC_CHECK(std::same_as, int>); + STATIC_CHECK(std::same_as, int>); + STATIC_CHECK(std::same_as, unsigned>); + STATIC_CHECK(std::same_as, double>); + STATIC_CHECK(std::same_as, int>); + STATIC_CHECK(!has_dominant_type); // `std::common_type_t` is `int` + STATIC_CHECK(!has_dominant_type); + STATIC_CHECK(!has_dominant_type); +} + +TEMPLATE_TEST_CASE("dominant_type", "[type_traits]", IRIS_TEST_NARROW_FP_PAIRS) +{ + using F16 = TestType::first_type; + using BF16 = TestType::second_type; + + STATIC_CHECK(std::same_as, F16>); + STATIC_CHECK(std::same_as, F16>); + STATIC_CHECK(!has_dominant_type); + STATIC_CHECK(std::same_as, float>); + + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, F16>); + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, F16>); + + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, F16>); + + STATIC_CHECK(!has_common_type); + STATIC_CHECK(!has_dominant_type); + STATIC_CHECK(!has_dominant_type); + + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, float>); + + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(!has_common_type); + STATIC_CHECK(!has_common_type); + + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, float>); + + STATIC_CHECK(std::same_as, double>); + STATIC_CHECK(std::same_as, double>); + STATIC_CHECK(std::same_as, double>); + STATIC_CHECK(!has_common_type); + + STATIC_CHECK(!has_dominant_type); + STATIC_CHECK(!has_dominant_type); +} + +TEST_CASE("symmetric_common_type: basic", "[type_traits]") +{ + STATIC_CHECK(!has_symmetric_common_type<>); + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, float>); + + STATIC_CHECK(std::same_as, int>); + + STATIC_CHECK(!has_dominant_type); + STATIC_CHECK(std::same_as, int>); + STATIC_CHECK(std::same_as, int>); + STATIC_CHECK(std::same_as, int>); + STATIC_CHECK(std::same_as, int>); + STATIC_CHECK(std::same_as, int>); +} + +TEMPLATE_TEST_CASE("symmetric_common_type", "[type_traits]", IRIS_TEST_NARROW_FP_PAIRS) +{ + using F16 = TestType::first_type; + using BF16 = TestType::second_type; + + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, float>); + STATIC_CHECK(std::same_as, double>); + + STATIC_CHECK(!has_symmetric_common_type); + STATIC_CHECK(!has_symmetric_common_type); + STATIC_CHECK(!has_symmetric_common_type); +} + +#undef IRIS_TEST_NARROW_FP_PAIRS + TEST_CASE("is_convertible_without_narrowing: same type identity") { STATIC_CHECK(iris::is_convertible_without_narrowing_v); diff --git a/test/units/concepts.cpp b/test/units/concepts.cpp new file mode 100644 index 0000000..56bc878 --- /dev/null +++ b/test/units/concepts.cpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +#include "iris_test.hpp" + +#include +#include + +#include +#include + +template +struct Length : iris::units::quantity +{ + using iris::units::quantity::quantity; +}; + +TEST_CASE("concepts", "[units]") +{ + STATIC_CHECK(iris::units::ordered_linear); + STATIC_CHECK(iris::units::nothrow_ordered_linear); + + STATIC_CHECK(iris::units::ordered_linear); + STATIC_CHECK(iris::units::nothrow_ordered_linear); + + STATIC_CHECK(!iris::units::ordered_linear); + + STATIC_CHECK(iris::units::ordered_linear>); + STATIC_CHECK(iris::units::nothrow_ordered_linear>); + + STATIC_CHECK(iris::units::ordered_linear>); + STATIC_CHECK(iris::units::nothrow_ordered_linear>); + + STATIC_CHECK(!iris::units::ordered_linear>); + + STATIC_CHECK(iris::units::ordered_linear>); + STATIC_CHECK(iris::units::nothrow_ordered_linear>); + + STATIC_CHECK(iris::units::ordered_linear>); + STATIC_CHECK(iris::units::nothrow_ordered_linear>); + + STATIC_CHECK(!iris::units::ordered_linear>); +} diff --git a/test/units/quantity.cpp b/test/units/quantity.cpp index 00f073e..8af0084 100644 --- a/test/units/quantity.cpp +++ b/test/units/quantity.cpp @@ -14,8 +14,8 @@ #include //using iris::units::quantity; -using iris::units::quantity_like; -using iris::units::quantity_traits; +using iris::units::quantity_class; +using iris::units::unit_traits; template concept has_plus = requires(A a, B b) { a + b; }; @@ -89,22 +89,22 @@ IRIS_QUANTITY_DEDUCTION_GUIDE(my_quantity); // --------------------------------------------------- -TEST_CASE("basic type traits") +TEST_CASE("basic type traits", "[units]") { STATIC_CHECK(!std::is_constructible_v>); // must be derived - STATIC_CHECK(quantity_like>); - STATIC_CHECK(quantity_like>); - STATIC_CHECK(std::same_as>::value_type, float>); - STATIC_CHECK(std::same_as>::rebind, my_quantity>); + STATIC_CHECK(quantity_class>); + STATIC_CHECK(quantity_class>); + STATIC_CHECK(std::same_as>::value_type, float>); + STATIC_CHECK(std::same_as>::rebind, my_quantity>); - STATIC_CHECK(!quantity_like); - STATIC_CHECK(!quantity_like); - STATIC_CHECK(!quantity_like*>); - STATIC_CHECK(!quantity_like const>); - STATIC_CHECK(!quantity_like&>); + STATIC_CHECK(!quantity_class); + STATIC_CHECK(!quantity_class); + STATIC_CHECK(!quantity_class*>); + STATIC_CHECK(quantity_class const>); + STATIC_CHECK(quantity_class&>); - STATIC_CHECK(!quantity_like>); + STATIC_CHECK(!quantity_class>); // ------------------------------------------------- @@ -537,9 +537,9 @@ IRIS_QUANTITY_DEDUCTION_GUIDE(Count); TEST_CASE("derived: type traits", "[units][quantity]") { - STATIC_CHECK(quantity_like>); - STATIC_CHECK(quantity_like>); - STATIC_CHECK(std::same_as>::rebind, RelativeLength>); + STATIC_CHECK(quantity_class>); + STATIC_CHECK(quantity_class>); + STATIC_CHECK(std::same_as>::rebind, RelativeLength>); STATIC_CHECK(std::same_as, RelativeLength>, RelativeLength>); STATIC_CHECK(std::same_as, RelativeLength>, RelativeLength>); STATIC_CHECK(std::same_as>::max()), RelativeLength>); From 6f5b16f1a6a7ce1583de2a75d08a5456d87d516d Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:43:42 +0900 Subject: [PATCH 02/14] Conditionally include `` --- test/type_traits.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/type_traits.cpp b/test/type_traits.cpp index 4646997..f941dea 100644 --- a/test/type_traits.cpp +++ b/test/type_traits.cpp @@ -9,7 +9,10 @@ #include #include #include -#include + +#if __has_include() +# include +#endif #include From 140bdb8e8a7c13fe7123ee7ac965fa47e884ad46 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Fri, 18 Sep 2026 03:14:29 +0900 Subject: [PATCH 03/14] Implement `min_pref_max` --- include/iris/math.hpp | 51 +++++ include/iris/units/min_pref_max.hpp | 329 ++++++++++++++++++++++++++++ include/iris/units/quantity.hpp | 11 + include/iris/units/traits.hpp | 3 +- test/CMakeLists.txt | 2 + test/math.cpp | 78 +++++++ test/units/min_pref_max.cpp | 210 ++++++++++++++++++ 7 files changed, 683 insertions(+), 1 deletion(-) create mode 100644 include/iris/math.hpp create mode 100644 include/iris/units/min_pref_max.hpp create mode 100644 test/math.cpp create mode 100644 test/units/min_pref_max.cpp diff --git a/include/iris/math.hpp b/include/iris/math.hpp new file mode 100644 index 0000000..89079a6 --- /dev/null +++ b/include/iris/math.hpp @@ -0,0 +1,51 @@ +#ifndef IRIS_ZZ_MATH_HPP +#define IRIS_ZZ_MATH_HPP + +// SPDX-License-Identifier: MIT + +#include + +#include +#include +#include + +#include // IWYU pragma: keep +#include // IWYU pragma: keep + +namespace iris { + +namespace detail { + +template +concept bit_inspectable_floating_point = + std::floating_point && + std::numeric_limits::radix == 2 && + requires { typename unsigned_integer_of_size_t; }; + +} // detail + +template +[[nodiscard]] constexpr bool isnan(T const x) noexcept +{ + if constexpr (detail::bit_inspectable_floating_point) { + using uint = unsigned_integer_of_size_t; + constexpr int mantissa_bits = std::numeric_limits::digits - 1; + constexpr int exponent_bits = int(sizeof(T) * 8) - 1 - mantissa_bits; + constexpr uint mantissa_mask = (uint(1) << mantissa_bits) - 1; + constexpr uint exponent_mask = ((uint(1) << exponent_bits) - 1) << mantissa_bits; + uint const bits = std::bit_cast(x); + return (bits & exponent_mask) == exponent_mask && (bits & mantissa_mask) != 0; + } else { + return x != x; + } +} + +template +[[nodiscard]] constexpr bool isnan(T) noexcept +{ + return false; +} + +} // iris + +#endif diff --git a/include/iris/units/min_pref_max.hpp b/include/iris/units/min_pref_max.hpp new file mode 100644 index 0000000..4947e9d --- /dev/null +++ b/include/iris/units/min_pref_max.hpp @@ -0,0 +1,329 @@ +#ifndef IRIS_ZZ_UNITS_MIN_PREF_MAX_HPP +#define IRIS_ZZ_UNITS_MIN_PREF_MAX_HPP + +// SPDX-License-Identifier: MIT + +#include // IWYU pragma: keep + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include // IWYU pragma: keep +#include + +#include +#include // IWYU pragma: keep + +namespace iris::units { + +template +class min_pref_max; + +template class DerivedTT, ordered_linear T, class... Rest> + requires + std::derived_from, min_pref_max> && + (!std::same_as, min_pref_max>) +struct unit_traits> +{ + using value_type = T; + + template + using base_rebind = min_pref_max; + + template + using rebind = DerivedTT; +}; + +template +concept min_pref_max_class = unit_class_of; + +template +concept min_pref_max_family = unit_family_of; + +// Declares the deduction guide for a min_pref_max family, so that `DerivedClass{min, pref, max}` +// can be deduced. +// Not required on compilers that implement . +#define IRIS_MIN_PREF_MAX_DEDUCTION_GUIDE(class_name, ...) \ + template \ + requires ::iris::units::ordered_linear<::iris::symmetric_common_type_t> \ + class_name(MinT, PrefT, MaxT) -> class_name<::iris::symmetric_common_type_t __VA_OPT__(,) __VA_ARGS__> + +// `min_pref_max` holds three components `T min`, `T pref`, and `T max`, with the +// invariant `min <= pref <= max`. It serves as a base type for settings in typesetting +// applications: for instance, a "margin" setting is naturally expressed as the set of +// "min margin", "preferred margin", and "max margin". +// +// When setting a new value to a component, other components are automatically clamped +// to satisfy the invariant `min <= pref <= max`. +// +// Applying `final` to the derived class is strongly advised. +// +// For practical ergonomics, the derived class should provide the following: +// +// 1. `using iris::units::min_pref_max::min_pref_max;` +// Required. Without it, the class has no usable constructor and the operators +// fail to instantiate. +// +// 2. `IRIS_MIN_PREF_MAX_DEDUCTION_GUIDE(class_name);` +// Enables construction with the syntax `DerivedClass{min, pref, max}`. +// Not required on compilers that implement . +template +class min_pref_max +{ + static_assert(ordered_linear); + static_assert( + nothrow_ordered_linear, + "In order to provide strong exception guarantee, `min_pref_max` naturally requires " + "the entire operation to be atomic (i.e., each component-wise operation must not throw)." + ); + + template + friend class min_pref_max; + +public: + using value_type = T; + + [[nodiscard]] constexpr T const& min() const noexcept { return min_; } + [[nodiscard]] constexpr T const& pref() const noexcept { return pref_; } + [[nodiscard]] constexpr T const& max() const noexcept { return max_; } + + [[nodiscard]] constexpr T shrink() const noexcept { return pref_ - min_; } + [[nodiscard]] constexpr T stretch() const noexcept { return max_ - pref_; } + + [[nodiscard]] constexpr bool is_all_zero() const noexcept { return min_ == T{} && pref_ == T{} && max_ == T{}; } + + constexpr void set_min(T v) noexcept + { + #if __cpp_lib_constexpr_cmath >= 202202L + assert(!isnan(v)); + #else + assert(v == v); + #endif + min_ = v; + pref_ = std::max(pref_, v); + max_ = std::max(max_, v); + this->check_invariants(); + } + constexpr void set_pref(T v) noexcept + { + #if __cpp_lib_constexpr_cmath >= 202202L + assert(!isnan(v)); + #else + assert(v == v); + #endif + min_ = std::min(min_, v); + pref_ = v; + max_ = std::max(max_, v); + this->check_invariants(); + } + constexpr void set_max(T v) noexcept + { + #if __cpp_lib_constexpr_cmath >= 202202L + assert(!isnan(v)); + #else + assert(v == v); + #endif + min_ = std::min(min_, v); + pref_ = std::min(pref_, v); + max_ = v; + this->check_invariants(); + } + + // --------------------------------------------------- + + template + requires min_pref_max_family + [[nodiscard]] constexpr bool + operator==(this Self const& self, Other const& other) noexcept + { + using common = common_value_type_t; + return static_cast(self.min_) == static_cast(other.min_) && + static_cast(self.pref_) == static_cast(other.pref_) && + static_cast(self.max_) == static_cast(other.max_); + } + + // --------------------------------------------------- + + constexpr min_pref_max() = default; + +protected: + // Using `min_pref_max` without deriving is prohibited as a "min_pref_max" without + // target domain is meaningless + constexpr ~min_pref_max() noexcept = default; + +public: + // We need to resurrect defaulted special members since we declare destructor + constexpr min_pref_max(min_pref_max const&) noexcept = default; + constexpr min_pref_max(min_pref_max&&) noexcept = default; + constexpr min_pref_max& operator=(min_pref_max const&) noexcept = default; + constexpr min_pref_max& operator=(min_pref_max&&) noexcept = default; + + template + requires + std::constructible_from && + std::constructible_from && + std::constructible_from + constexpr explicit(!( + std::convertible_to && + std::convertible_to && + std::convertible_to + )) min_pref_max(MinT min, PrefT pref, MaxT max) + : min_(std::move(min)) + , pref_(std::move(pref)) + , max_(std::move(max)) + { + if (!(min_ <= pref_ && pref_ <= max_)) { + throwf("min <= pref <= max: got {}/{}/{}", min_, pref_, max_); + } + } + +private: + constexpr explicit min_pref_max(detail::invariants_always_satisfied_t, T min, T pref, T max) noexcept + : min_(std::move(min)) + , pref_(std::move(pref)) + , max_(std::move(max)) + { + check_invariants(); // assert + } + +public: + template + requires + (!std::same_as) && + min_pref_max_family + [[nodiscard]] constexpr explicit(!dominant_unit) + operator Target(this Self const& self) noexcept + { + using target_value_type = detail::value_type_t; + return Target( + detail::invariants_always_satisfied, + static_cast(self.min_), + static_cast(self.pref_), + static_cast(self.max_) + ); + } + + // --------------------------------------------------- + + template + requires min_pref_max_family + [[nodiscard]] constexpr std::common_type_t + operator+(this Self const& self, Other const& other) noexcept + { + using result = std::common_type_t; + using common = detail::value_type_t; + return result( + detail::invariants_always_satisfied, + static_cast(self.min_ + other.min_), + static_cast(self.pref_ + other.pref_), + static_cast(self.max_ + other.max_) + ); + } + + template + requires dominant_unit + constexpr Self& + operator+=(this Self& self, Other const& other) noexcept + { + self.min_ += other.min_; + self.pref_ += other.pref_; + self.max_ += other.max_; + self.check_invariants(); + return self; + } + + // ---------------------------------------------------- + + template + [[nodiscard]] constexpr Self + operator+(this Self const& self, T const& delta) noexcept + { + return Self(detail::invariants_always_satisfied, self.min_ + delta, self.pref_ + delta, self.max_ + delta); + } + + template + [[nodiscard]] constexpr Self + operator-(this Self const& self, T const& delta) noexcept + { + return Self(detail::invariants_always_satisfied, self.min_ - delta, self.pref_ - delta, self.max_ - delta); + } + + template + constexpr Self& + operator+=(this Self& self, T const& delta) noexcept + { + self.min_ += delta; + self.pref_ += delta; + self.max_ += delta; + self.check_invariants(); + return self; + } + + template + constexpr Self& + operator-=(this Self& self, T const& delta) noexcept + { + self.min_ -= delta; + self.pref_ -= delta; + self.max_ -= delta; + self.check_invariants(); + return self; + } + + template + requires std::derived_from // required for avoiding ODR violation + [[nodiscard]] friend constexpr M operator+(T const& delta, M const& m) noexcept + { + return m + delta; + } + +private: + constexpr void check_invariants() const noexcept + { + assert(min_ <= pref_); + assert(pref_ <= max_); + } + + T min_{}, pref_{}, max_{}; +}; + +} // iris::units + +template +struct std::hash +{ + [[nodiscard]] static std::size_t operator()(M const& m) noexcept + { + return iris::hash_all(m.min(), m.pref(), m.max()); + } +}; + +template +struct std::formatter : std::formatter, CharT> +{ + using base = std::formatter, CharT>; + + template + auto format(M const& m, FormatContext& ctx) const + { + auto out = base::format(m.min(), ctx); + *out++ = CharT('/'); + ctx.advance_to(out); + out = base::format(m.pref(), ctx); + *out++ = CharT('/'); + ctx.advance_to(out); + return base::format(m.max(), ctx); + } +}; + +#endif diff --git a/include/iris/units/quantity.hpp b/include/iris/units/quantity.hpp index fa2c126..04b5d84 100644 --- a/include/iris/units/quantity.hpp +++ b/include/iris/units/quantity.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -395,6 +396,16 @@ class std::numeric_limits> : public std::numeric_limits namespace iris::units { +using iris::isnan; + +template +[[nodiscard]] constexpr bool isnan(Q const& q) noexcept +{ + return iris::isnan(q.value); +} + +// -------------------------------------------------- + template requires std::floating_point> [[nodiscard]] constexpr Q trunc(Q const& q) noexcept diff --git a/include/iris/units/traits.hpp b/include/iris/units/traits.hpp index 8d947f7..84c5e74 100644 --- a/include/iris/units/traits.hpp +++ b/include/iris/units/traits.hpp @@ -21,7 +21,8 @@ concept compatible_value_type = }; template -struct unit_traits; +struct unit_traits +{}; namespace detail { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6adb446..403b218 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -200,6 +200,7 @@ if(PROJECT_IS_TOP_LEVEL) indexed_value run_length_sequence bitset + math ) foreach(test_name IN LISTS IRIS_TEST_IRIS_TESTS) iris_define_internal_test(${test_name} ${test_name}.cpp) @@ -211,6 +212,7 @@ if(PROJECT_IS_TOP_LEVEL) IRIS_TEST_UNITS_TESTS concepts quantity + min_pref_max ) foreach(test_name IN LISTS IRIS_TEST_UNITS_TESTS) iris_define_internal_subdir_test(units ${test_name} ${test_name}.cpp) diff --git a/test/math.cpp b/test/math.cpp new file mode 100644 index 0000000..116dedc --- /dev/null +++ b/test/math.cpp @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT + +#include "iris_test.hpp" + +#include + +#include + +#include +#include + +#include +#include + +namespace { + +template struct uint_of_size; +template<> struct uint_of_size<2> { using type = std::uint16_t; }; +template<> struct uint_of_size<4> { using type = std::uint32_t; }; +template<> struct uint_of_size<8> { using type = std::uint64_t; }; + +#define IRIS_TEST_ISNAN_TYPES float, double, long double + +} // anonymous + +TEMPLATE_TEST_CASE("isnan vs std::isnan", "[math]", IRIS_TEST_ISNAN_TYPES) +{ + using T = TestType; + using limits = std::numeric_limits; + + STATIC_CHECK(!iris::isnan(T{})); + STATIC_CHECK(!iris::isnan(T{1})); + STATIC_CHECK(!iris::isnan(limits::infinity())); + STATIC_CHECK(!iris::isnan(-limits::infinity())); + STATIC_CHECK(!iris::isnan((limits::max)())); + STATIC_CHECK(!iris::isnan(limits::denorm_min())); + STATIC_CHECK(iris::isnan(limits::quiet_NaN())); + STATIC_CHECK(iris::isnan(-limits::quiet_NaN())); + STATIC_CHECK(iris::isnan(limits::signaling_NaN())); + + for (T const x : { + T{}, -T{}, T{1}, T{-1}, + (limits::min)(), (limits::max)(), limits::lowest(), limits::epsilon(), limits::denorm_min(), + limits::infinity(), -limits::infinity(), + limits::quiet_NaN(), -limits::quiet_NaN(), limits::signaling_NaN(), + }) { + CHECK(iris::isnan(x) == std::isnan(x)); + } + + if constexpr (sizeof(T) <= 8) { + using uint = uint_of_size::type; + constexpr int mantissa_bits = limits::digits - 1; + constexpr uint mantissa_mask = (uint(1) << mantissa_bits) - 1; + constexpr uint exponent_all_ones = ~mantissa_mask & (uint(-1) >> 1); + constexpr uint sign = uint(1) << (sizeof(T) * 8 - 1); + + auto const check_bits = [](uint const bits) { + T const x = std::bit_cast(bits); + CHECK(iris::isnan(x) == std::isnan(x)); + }; + + // Infinity and NaNs with small payloads, both signs + for (uint payload = 0; payload < 256; ++payload) { + check_bits(exponent_all_ones | payload); + check_bits(sign | exponent_all_ones | payload); + } + // Largest finite values, both signs + for (uint payload = 0; payload < 256; ++payload) { + check_bits((exponent_all_ones - (uint(1) << mantissa_bits)) | (mantissa_mask - payload)); + check_bits(sign | (exponent_all_ones - (uint(1) << mantissa_bits)) | (mantissa_mask - payload)); + } + // NaN with only the top mantissa bit set (quiet NaN on most platforms) and only the lowest bit set + check_bits(exponent_all_ones | (uint(1) << (mantissa_bits - 1))); + check_bits(exponent_all_ones | uint(1)); + } +} + +#undef IRIS_TEST_ISNAN_TYPES diff --git a/test/units/min_pref_max.cpp b/test/units/min_pref_max.cpp new file mode 100644 index 0000000..46796d3 --- /dev/null +++ b/test/units/min_pref_max.cpp @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: MIT + +#include "iris_test.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +using iris::units::min_pref_max; + +template +concept compound_addable = requires(L& lhs, R const& rhs) { lhs += rhs; }; + +template +concept compound_subtractable = requires(L& lhs, R const& rhs) { lhs -= rhs; }; + +template +concept subtractable = requires(L const& lhs, R const& rhs) { lhs - rhs; }; + +template +struct RelativeLength final : iris::units::quantity +{ + using iris::units::quantity::quantity; +}; +IRIS_QUANTITY_DEDUCTION_GUIDE(RelativeLength); + +template +struct MinPrefMax final : min_pref_max +{ + using min_pref_max::min_pref_max; +}; +IRIS_MIN_PREF_MAX_DEDUCTION_GUIDE(MinPrefMax); + +using MPM_d = MinPrefMax; +using MPM_f = MinPrefMax; +using MPM_rel_d = MinPrefMax>; +using MPM_rel_f = MinPrefMax>; + +TEST_CASE("type traits", "[units][min_pref_max]") +{ + STATIC_CHECK(std::is_trivially_copyable_v); + STATIC_CHECK(std::is_nothrow_copy_constructible_v); + STATIC_CHECK(std::is_nothrow_move_assignable_v); + STATIC_CHECK(std::is_nothrow_default_constructible_v); + STATIC_CHECK(std::is_standard_layout_v); + STATIC_CHECK(!std::is_aggregate_v); + STATIC_CHECK(std::is_trivially_copyable_v); + + STATIC_CHECK(iris::units::min_pref_max_class); + STATIC_CHECK(iris::units::min_pref_max_class); + STATIC_CHECK(!iris::units::min_pref_max_class>); + STATIC_CHECK(!iris::units::min_pref_max_class>); + STATIC_CHECK(iris::units::min_pref_max_family); + STATIC_CHECK(iris::units::min_pref_max_family); + STATIC_CHECK(!iris::units::min_pref_max_family); + + STATIC_CHECK(std::same_as, MPM_d>); + STATIC_CHECK(std::same_as, MPM_rel_d>); + STATIC_CHECK(std::same_as); +} + +TEST_CASE("construction", "[units][min_pref_max]") +{ + constexpr MPM_d zero{}; + STATIC_CHECK(zero.is_all_zero()); + STATIC_CHECK(zero.min() == 0.0); + STATIC_CHECK(zero.pref() == 0.0); + STATIC_CHECK(zero.max() == 0.0); + + constexpr MPM_d m{0.0, 0.5, 1.0}; + STATIC_CHECK(m.min() == 0.0); + STATIC_CHECK(m.pref() == 0.5); + STATIC_CHECK(m.max() == 1.0); + STATIC_CHECK(m.shrink() == 0.5); + STATIC_CHECK(m.stretch() == 0.5); + STATIC_CHECK(!m.is_all_zero()); + + constexpr MPM_d hetero{0, 0.5f, 1}; + STATIC_CHECK(hetero == m); + constexpr MPM_d degenerate{1.0, 1.0, 1.0}; + STATIC_CHECK(degenerate.shrink() == 0.0); + STATIC_CHECK(degenerate.stretch() == 0.0); + + constexpr MPM_rel_d rel{RelativeLength{0.0}, RelativeLength{0.5}, RelativeLength{1.0}}; + STATIC_CHECK(rel.pref() == RelativeLength{0.5}); + STATIC_CHECK(rel.stretch() == RelativeLength{0.5}); + + REQUIRE_THROWS_AS((MPM_d{0.5, 0.0, 1.0}), std::invalid_argument); + REQUIRE_THROWS_AS((MPM_d{0.0, 1.0, 0.5}), std::invalid_argument); + constexpr double nan = std::numeric_limits::quiet_NaN(); + REQUIRE_THROWS_AS((MPM_d{nan, 0.5, 1.0}), std::invalid_argument); + REQUIRE_THROWS_AS((MPM_d{0.0, nan, 1.0}), std::invalid_argument); + REQUIRE_THROWS_AS((MPM_d{0.0, 0.5, nan}), std::invalid_argument); +} + +TEST_CASE("conversion", "[units][min_pref_max]") +{ + STATIC_CHECK(std::is_convertible_v); + STATIC_CHECK(!std::is_convertible_v); + STATIC_CHECK(std::is_constructible_v); + STATIC_CHECK(std::is_convertible_v); + STATIC_CHECK(!std::is_convertible_v); + STATIC_CHECK(!std::is_constructible_v); + + constexpr MPM_f f{0.0f, 0.5f, 1.0f}; + constexpr MPM_d d = f; + STATIC_CHECK(d.pref() == 0.5); + constexpr MPM_f back{d}; + STATIC_CHECK(back == f); +} + +TEST_CASE("comparison", "[units][min_pref_max]") +{ + constexpr MPM_d a{0.0, 0.5, 1.0}; + constexpr MPM_d b{0.0, 0.5, 1.0}; + constexpr MPM_d c{0.0, 0.25, 1.0}; + constexpr MPM_f af{0.0f, 0.5f, 1.0f}; + + STATIC_CHECK(a == b); + STATIC_CHECK(a != c); + STATIC_CHECK(a == af); + STATIC_CHECK(af == a); + CHECK(std::hash{}(a) == std::hash{}(b)); +} + +TEST_CASE("addition", "[units][min_pref_max]") +{ + constexpr MPM_d a{0.0, 0.5, 1.0}; + constexpr MPM_d b{0.25, 0.5, 2.0}; + constexpr MPM_f bf{0.25f, 0.5f, 2.0f}; + + STATIC_CHECK(a + b == MPM_d{0.25, 1.0, 3.0}); + STATIC_CHECK(std::same_as); + STATIC_CHECK(std::same_as); + STATIC_CHECK(a + bf == MPM_d{0.25, 1.0, 3.0}); + + constexpr auto compound = [] { + MPM_d m{0.0, 0.5, 1.0}; + m += MPM_f{0.25f, 0.5f, 2.0f}; + return m; + }(); + STATIC_CHECK(compound == MPM_d{0.25, 1.0, 3.0}); + + STATIC_CHECK(compound_addable); + STATIC_CHECK(!compound_addable); + + STATIC_CHECK(!subtractable); + STATIC_CHECK(!compound_subtractable); +} + +TEST_CASE("shift", "[units][min_pref_max]") +{ + constexpr MPM_d m{0.0, 0.5, 1.0}; + + STATIC_CHECK(m + 1.0 == MPM_d{1.0, 1.5, 2.0}); + STATIC_CHECK(1.0 + m == MPM_d{1.0, 1.5, 2.0}); + STATIC_CHECK(m - 1.0 == MPM_d{-1.0, -0.5, 0.0}); + STATIC_CHECK(std::same_as); + STATIC_CHECK(!subtractable); // `1.0 - m` + + constexpr auto shifted = [] { + MPM_d x{0.0, 0.5, 1.0}; + x += 1.0; + x -= 0.5; + return x; + }(); + STATIC_CHECK(shifted == MPM_d{0.5, 1.0, 1.5}); + STATIC_CHECK(shifted.shrink() == 0.5); + STATIC_CHECK(shifted.stretch() == 0.5); + + constexpr MPM_rel_d rel{RelativeLength{0.0}, RelativeLength{0.5}, RelativeLength{1.0}}; + STATIC_CHECK((rel + RelativeLength{1.0}).min() == RelativeLength{1.0}); +} + +TEST_CASE("setters", "[units][min_pref_max]") +{ + constexpr auto set = [](auto setter) { + MPM_d m{0.0, 0.5, 1.0}; + setter(m); + return m; + }; + + STATIC_CHECK(set([](MPM_d& m) { m.set_min(-1.0); }) == MPM_d{-1.0, 0.5, 1.0}); + STATIC_CHECK(set([](MPM_d& m) { m.set_min(0.7); }) == MPM_d{0.7, 0.7, 1.0}); + STATIC_CHECK(set([](MPM_d& m) { m.set_min(2.0); }) == MPM_d{2.0, 2.0, 2.0}); + + STATIC_CHECK(set([](MPM_d& m) { m.set_max(3.0); }) == MPM_d{0.0, 0.5, 3.0}); + STATIC_CHECK(set([](MPM_d& m) { m.set_max(0.25); }) == MPM_d{0.0, 0.25, 0.25}); + STATIC_CHECK(set([](MPM_d& m) { m.set_max(-1.0); }) == MPM_d{-1.0, -1.0, -1.0}); + + STATIC_CHECK(set([](MPM_d& m) { m.set_pref(0.75); }) == MPM_d{0.0, 0.75, 1.0}); + STATIC_CHECK(set([](MPM_d& m) { m.set_pref(-1.0); }) == MPM_d{-1.0, -1.0, 1.0}); + STATIC_CHECK(set([](MPM_d& m) { m.set_pref(2.0); }) == MPM_d{0.0, 2.0, 2.0}); +} + +TEST_CASE("format", "[units][min_pref_max]") +{ + MPM_d const m{0.0, 0.5, 1.0}; + CHECK(std::format("{}", m) == "0/0.5/1"); + CHECK(std::format("{:.2f}", m) == "0.00/0.50/1.00"); + + MPM_rel_d const rel{RelativeLength{0.0}, RelativeLength{0.5}, RelativeLength{1.0}}; + CHECK(std::format("{:.1f}", rel) == "0.0/0.5/1.0"); +} From 04ead47c21a66e88078cbd411ad349adc2b3da39 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Fri, 18 Sep 2026 03:24:54 +0900 Subject: [PATCH 04/14] Fix variadic concept --- include/iris/units/traits.hpp | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/include/iris/units/traits.hpp b/include/iris/units/traits.hpp index 84c5e74..79645a0 100644 --- a/include/iris/units/traits.hpp +++ b/include/iris/units/traits.hpp @@ -14,10 +14,10 @@ namespace iris::units { -template +template concept compatible_value_type = requires { - typename symmetric_common_type_t; + typename symmetric_common_type_t; }; template @@ -96,18 +96,19 @@ concept unit_family_with = detail::same_unit_class_impl, std::remove_cvref_t> && compatible_value_type, detail::value_type_t>; -// All of `First, Rest...` are specializations of the same unit class template, and -// their value types are pairwise compatible with that of `First`. -template +// All of `Units...` are specializations of the same unit class template, and +// their value types are pairwise compatible with that of the first type in `Units...`. +template concept unit_family = - unit_class && - (unit_family_with && ...); + sizeof...(Units) > 0 && + (unit_class && ...) && + (unit_family_with && ...); -// `unit_family` whose base unit class is `UnitTT`. -template class UnitTT, class First, class... Rest> +// `unit_family` whose base unit class is `UnitTT`. +template class UnitTT, class... Units> concept unit_family_of = - unit_family && - unit_class_of; + unit_family && + unit_class_of; } // iris::units @@ -157,11 +158,19 @@ namespace iris::units { // The value type shared by the units in `Units...`, i.e., `symmetric_common_type_t` // of their value types. +template +struct common_value_type +{}; +template +using common_value_type_t = common_value_type::type; + template requires unit_family && compatible_value_type...> -using common_value_type_t = symmetric_common_type_t...>; +struct common_value_type + : symmetric_common_type...> +{}; // The unit type of `First` rebound to the common value type of `First, Rest...`. template From a20577ee4b65710f4497b396e3b0ea0f670f707b Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Fri, 18 Sep 2026 03:26:24 +0900 Subject: [PATCH 05/14] Fix isnan --- include/iris/math.hpp | 4 ++++ include/iris/units/min_pref_max.hpp | 12 ------------ test/math.cpp | 14 +------------- 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/include/iris/math.hpp b/include/iris/math.hpp index 89079a6..173fddd 100644 --- a/include/iris/math.hpp +++ b/include/iris/math.hpp @@ -27,6 +27,9 @@ concept bit_inspectable_floating_point = template [[nodiscard]] constexpr bool isnan(T const x) noexcept { +#if __cpp_lib_constexpr_cmath >= 202202L + return std::isnan(x); +#else if constexpr (detail::bit_inspectable_floating_point) { using uint = unsigned_integer_of_size_t; constexpr int mantissa_bits = std::numeric_limits::digits - 1; @@ -38,6 +41,7 @@ template } else { return x != x; } +#endif } template diff --git a/include/iris/units/min_pref_max.hpp b/include/iris/units/min_pref_max.hpp index 4947e9d..d3bd48d 100644 --- a/include/iris/units/min_pref_max.hpp +++ b/include/iris/units/min_pref_max.hpp @@ -104,11 +104,7 @@ class min_pref_max constexpr void set_min(T v) noexcept { - #if __cpp_lib_constexpr_cmath >= 202202L assert(!isnan(v)); - #else - assert(v == v); - #endif min_ = v; pref_ = std::max(pref_, v); max_ = std::max(max_, v); @@ -116,11 +112,7 @@ class min_pref_max } constexpr void set_pref(T v) noexcept { - #if __cpp_lib_constexpr_cmath >= 202202L assert(!isnan(v)); - #else - assert(v == v); - #endif min_ = std::min(min_, v); pref_ = v; max_ = std::max(max_, v); @@ -128,11 +120,7 @@ class min_pref_max } constexpr void set_max(T v) noexcept { - #if __cpp_lib_constexpr_cmath >= 202202L assert(!isnan(v)); - #else - assert(v == v); - #endif min_ = std::min(min_, v); pref_ = std::min(pref_, v); max_ = v; diff --git a/test/math.cpp b/test/math.cpp index 116dedc..c2d77e7 100644 --- a/test/math.cpp +++ b/test/math.cpp @@ -12,17 +12,8 @@ #include #include -namespace { - -template struct uint_of_size; -template<> struct uint_of_size<2> { using type = std::uint16_t; }; -template<> struct uint_of_size<4> { using type = std::uint32_t; }; -template<> struct uint_of_size<8> { using type = std::uint64_t; }; - #define IRIS_TEST_ISNAN_TYPES float, double, long double -} // anonymous - TEMPLATE_TEST_CASE("isnan vs std::isnan", "[math]", IRIS_TEST_ISNAN_TYPES) { using T = TestType; @@ -48,7 +39,7 @@ TEMPLATE_TEST_CASE("isnan vs std::isnan", "[math]", IRIS_TEST_ISNAN_TYPES) } if constexpr (sizeof(T) <= 8) { - using uint = uint_of_size::type; + using uint = iris::unsigned_integer_of_size_t; constexpr int mantissa_bits = limits::digits - 1; constexpr uint mantissa_mask = (uint(1) << mantissa_bits) - 1; constexpr uint exponent_all_ones = ~mantissa_mask & (uint(-1) >> 1); @@ -59,17 +50,14 @@ TEMPLATE_TEST_CASE("isnan vs std::isnan", "[math]", IRIS_TEST_ISNAN_TYPES) CHECK(iris::isnan(x) == std::isnan(x)); }; - // Infinity and NaNs with small payloads, both signs for (uint payload = 0; payload < 256; ++payload) { check_bits(exponent_all_ones | payload); check_bits(sign | exponent_all_ones | payload); } - // Largest finite values, both signs for (uint payload = 0; payload < 256; ++payload) { check_bits((exponent_all_ones - (uint(1) << mantissa_bits)) | (mantissa_mask - payload)); check_bits(sign | (exponent_all_ones - (uint(1) << mantissa_bits)) | (mantissa_mask - payload)); } - // NaN with only the top mantissa bit set (quiet NaN on most platforms) and only the lowest bit set check_bits(exponent_all_ones | (uint(1) << (mantissa_bits - 1))); check_bits(exponent_all_ones | uint(1)); } From a96424b9241bb8927862f1bbc397f067eeeb3d0b Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Fri, 18 Sep 2026 03:38:25 +0900 Subject: [PATCH 06/14] Add empty class body --- include/iris/stdint.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/iris/stdint.hpp b/include/iris/stdint.hpp index 5065513..26e0005 100644 --- a/include/iris/stdint.hpp +++ b/include/iris/stdint.hpp @@ -20,7 +20,8 @@ namespace iris { namespace detail { template -struct integer_of_size_impl; // undefined +struct integer_of_size_impl +{}; template<> struct integer_of_size_impl From ddb4099b6bccff3d5f6ea7eabf6f4c2e361fb93a Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Fri, 18 Sep 2026 03:41:40 +0900 Subject: [PATCH 07/14] Remove overly strong assertion --- test/units/concepts.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/units/concepts.cpp b/test/units/concepts.cpp index 56bc878..d64cfdd 100644 --- a/test/units/concepts.cpp +++ b/test/units/concepts.cpp @@ -25,11 +25,7 @@ TEST_CASE("concepts", "[units]") STATIC_CHECK(!iris::units::ordered_linear); STATIC_CHECK(iris::units::ordered_linear>); - STATIC_CHECK(iris::units::nothrow_ordered_linear>); - STATIC_CHECK(iris::units::ordered_linear>); - STATIC_CHECK(iris::units::nothrow_ordered_linear>); - STATIC_CHECK(!iris::units::ordered_linear>); STATIC_CHECK(iris::units::ordered_linear>); From 3d0f3d02d1dd4275644508f0d28a4cd212c81755 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Fri, 18 Sep 2026 03:48:17 +0900 Subject: [PATCH 08/14] Remove chrono example from unit concept test --- test/units/concepts.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/units/concepts.cpp b/test/units/concepts.cpp index d64cfdd..106354a 100644 --- a/test/units/concepts.cpp +++ b/test/units/concepts.cpp @@ -5,7 +5,6 @@ #include #include -#include #include template @@ -24,10 +23,6 @@ TEST_CASE("concepts", "[units]") STATIC_CHECK(!iris::units::ordered_linear); - STATIC_CHECK(iris::units::ordered_linear>); - STATIC_CHECK(iris::units::ordered_linear>); - STATIC_CHECK(!iris::units::ordered_linear>); - STATIC_CHECK(iris::units::ordered_linear>); STATIC_CHECK(iris::units::nothrow_ordered_linear>); From 49df3cb16513b84106f0652125b2642023cf0a33 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Fri, 18 Sep 2026 03:50:27 +0900 Subject: [PATCH 09/14] Revert "Remove chrono example from unit concept test" This reverts commit 3d0f3d02d1dd4275644508f0d28a4cd212c81755. --- test/units/concepts.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/units/concepts.cpp b/test/units/concepts.cpp index 106354a..d64cfdd 100644 --- a/test/units/concepts.cpp +++ b/test/units/concepts.cpp @@ -5,6 +5,7 @@ #include #include +#include #include template @@ -23,6 +24,10 @@ TEST_CASE("concepts", "[units]") STATIC_CHECK(!iris::units::ordered_linear); + STATIC_CHECK(iris::units::ordered_linear>); + STATIC_CHECK(iris::units::ordered_linear>); + STATIC_CHECK(!iris::units::ordered_linear>); + STATIC_CHECK(iris::units::ordered_linear>); STATIC_CHECK(iris::units::nothrow_ordered_linear>); From fcfbd681ae43954b6a9bbf0f6b410f7fcbfcdd03 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Fri, 18 Sep 2026 03:51:34 +0900 Subject: [PATCH 10/14] Add type completeness check --- include/iris/units/min_pref_max.hpp | 1 + include/iris/units/quantity.hpp | 1 + 2 files changed, 2 insertions(+) diff --git a/include/iris/units/min_pref_max.hpp b/include/iris/units/min_pref_max.hpp index d3bd48d..87f893d 100644 --- a/include/iris/units/min_pref_max.hpp +++ b/include/iris/units/min_pref_max.hpp @@ -31,6 +31,7 @@ class min_pref_max; template class DerivedTT, ordered_linear T, class... Rest> requires + requires { sizeof(DerivedTT); } && std::derived_from, min_pref_max> && (!std::same_as, min_pref_max>) struct unit_traits> diff --git a/include/iris/units/quantity.hpp b/include/iris/units/quantity.hpp index 04b5d84..273953e 100644 --- a/include/iris/units/quantity.hpp +++ b/include/iris/units/quantity.hpp @@ -30,6 +30,7 @@ class quantity; template class DerivedTT, numeric_arithmetic T, class... Rest> requires + requires { sizeof(DerivedTT); } && std::derived_from, quantity> && (!std::same_as, quantity>) struct unit_traits> From e96a7355d305ac598067a8429ee7ea7b25681ad0 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Fri, 18 Sep 2026 04:04:39 +0900 Subject: [PATCH 11/14] Add temporary Clang check --- test/units/min_pref_max.cpp | 41 +++++++++++++++++++++++++++++++++++++ test/units/quantity.cpp | 1 - 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/test/units/min_pref_max.cpp b/test/units/min_pref_max.cpp index 46796d3..7b067c1 100644 --- a/test/units/min_pref_max.cpp +++ b/test/units/min_pref_max.cpp @@ -208,3 +208,44 @@ TEST_CASE("format", "[units][min_pref_max]") MPM_rel_d const rel{RelativeLength{0.0}, RelativeLength{0.5}, RelativeLength{1.0}}; CHECK(std::format("{:.1f}", rel) == "0.0/0.5/1.0"); } + + +template +concept has_common_type = requires { typename std::common_type::type; }; + +template +concept has_symmetric_common_type = requires { typename iris::symmetric_common_type::type; }; + +template +struct AbsoluteLength final : iris::units::quantity +{ + using iris::units::quantity::quantity; +}; +IRIS_QUANTITY_DEDUCTION_GUIDE(AbsoluteLength); + +// TODO: remove this +TEST_CASE("clang check") +{ + using RL = RelativeLength; + using AL = AbsoluteLength; + namespace detail = iris::units::detail; + + // 1 + STATIC_CHECK(std::same_as, RL>); + STATIC_CHECK(!std::same_as, RL>); + STATIC_CHECK(!detail::same_unit_class_impl); + + // 2 + STATIC_CHECK(!has_common_type); + STATIC_CHECK(!has_common_type); + STATIC_CHECK(!iris::dominant); + STATIC_CHECK(!iris::dominant); + STATIC_CHECK(!has_symmetric_common_type); + STATIC_CHECK(!iris::units::compatible_value_type); + + // 3 + STATIC_CHECK(!iris::units::unit_family_with); + STATIC_CHECK(!iris::units::unit_family); +} + + diff --git a/test/units/quantity.cpp b/test/units/quantity.cpp index 8af0084..157b827 100644 --- a/test/units/quantity.cpp +++ b/test/units/quantity.cpp @@ -57,7 +57,6 @@ template concept has_increment = requires(A& a) { ++a; a++; --a; a--; }; template -// ReSharper disable once CppUseTypeTraitAlias concept has_common_type = requires { typename std::common_type::type; }; template From fb60d65ff3c9de8305fa0915d37138d35c9a5407 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Fri, 18 Sep 2026 04:13:37 +0900 Subject: [PATCH 12/14] Disable native pack indexing --- include/iris/units/traits.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/iris/units/traits.hpp b/include/iris/units/traits.hpp index 79645a0..fdad893 100644 --- a/include/iris/units/traits.hpp +++ b/include/iris/units/traits.hpp @@ -102,13 +102,13 @@ template concept unit_family = sizeof...(Units) > 0 && (unit_class && ...) && - (unit_family_with && ...); + (unit_family_with, Units> && ...); // `unit_family` whose base unit class is `UnitTT`. template class UnitTT, class... Units> concept unit_family_of = unit_family && - unit_class_of; + unit_class_of, UnitTT>; } // iris::units From f219042b927d05fb44b55fdd18be337c657cec94 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Fri, 18 Sep 2026 04:22:23 +0900 Subject: [PATCH 13/14] Add clang workaround --- include/iris/type_traits.hpp | 4 +++- include/iris/units/traits.hpp | 4 ++-- test/units/min_pref_max.cpp | 41 ----------------------------------- 3 files changed, 5 insertions(+), 44 deletions(-) diff --git a/include/iris/type_traits.hpp b/include/iris/type_traits.hpp index 1215112..c0f35f9 100644 --- a/include/iris/type_traits.hpp +++ b/include/iris/type_traits.hpp @@ -193,7 +193,9 @@ using at_c_t = at_c::type; // Has native pack indexing? // Note: GCC 15 emits "sorry, unimplemented: mangling type pack index" -#if !(defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 15) && __cpp_pack_indexing >= 202311L +#if __cpp_pack_indexing >= 202311L && \ + !(defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 15) && \ + !defined(__clang__) # define IRIS_PACK_INDEXING(I, Ts_ellipsis) Ts_ellipsis[I] diff --git a/include/iris/units/traits.hpp b/include/iris/units/traits.hpp index fdad893..79645a0 100644 --- a/include/iris/units/traits.hpp +++ b/include/iris/units/traits.hpp @@ -102,13 +102,13 @@ template concept unit_family = sizeof...(Units) > 0 && (unit_class && ...) && - (unit_family_with, Units> && ...); + (unit_family_with && ...); // `unit_family` whose base unit class is `UnitTT`. template class UnitTT, class... Units> concept unit_family_of = unit_family && - unit_class_of, UnitTT>; + unit_class_of; } // iris::units diff --git a/test/units/min_pref_max.cpp b/test/units/min_pref_max.cpp index 7b067c1..46796d3 100644 --- a/test/units/min_pref_max.cpp +++ b/test/units/min_pref_max.cpp @@ -208,44 +208,3 @@ TEST_CASE("format", "[units][min_pref_max]") MPM_rel_d const rel{RelativeLength{0.0}, RelativeLength{0.5}, RelativeLength{1.0}}; CHECK(std::format("{:.1f}", rel) == "0.0/0.5/1.0"); } - - -template -concept has_common_type = requires { typename std::common_type::type; }; - -template -concept has_symmetric_common_type = requires { typename iris::symmetric_common_type::type; }; - -template -struct AbsoluteLength final : iris::units::quantity -{ - using iris::units::quantity::quantity; -}; -IRIS_QUANTITY_DEDUCTION_GUIDE(AbsoluteLength); - -// TODO: remove this -TEST_CASE("clang check") -{ - using RL = RelativeLength; - using AL = AbsoluteLength; - namespace detail = iris::units::detail; - - // 1 - STATIC_CHECK(std::same_as, RL>); - STATIC_CHECK(!std::same_as, RL>); - STATIC_CHECK(!detail::same_unit_class_impl); - - // 2 - STATIC_CHECK(!has_common_type); - STATIC_CHECK(!has_common_type); - STATIC_CHECK(!iris::dominant); - STATIC_CHECK(!iris::dominant); - STATIC_CHECK(!has_symmetric_common_type); - STATIC_CHECK(!iris::units::compatible_value_type); - - // 3 - STATIC_CHECK(!iris::units::unit_family_with); - STATIC_CHECK(!iris::units::unit_family); -} - - From 9d86e5f52bf357e9674372a34d01abc57c03c798 Mon Sep 17 00:00:00 2001 From: Nana Sakisaka <1901813+saki7@users.noreply.github.com> Date: Fri, 18 Sep 2026 17:33:02 +0900 Subject: [PATCH 14/14] Polish pack indexing --- include/iris/type_traits.hpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/include/iris/type_traits.hpp b/include/iris/type_traits.hpp index c0f35f9..7549206 100644 --- a/include/iris/type_traits.hpp +++ b/include/iris/type_traits.hpp @@ -158,8 +158,6 @@ struct constant_list static constexpr std::size_t size = sizeof...(Vals); }; -template using cvoid_t = void; - namespace detail { template @@ -169,7 +167,11 @@ template struct do_pack_indexing> { template - static T select(cvoid_t*..., std::type_identity*, ...); + static std::type_identity select( + decltype(void(Voids), static_cast(nullptr))..., + std::type_identity*, + ... + ); }; template @@ -179,7 +181,11 @@ template struct do_cpack_indexing> { template - static std::integral_constant select(cvoid_t*..., std::integral_constant*, ...); + static std::integral_constant select( + decltype(void(Voids), static_cast(nullptr))..., + std::integral_constant*, + ... + ); }; } // detail @@ -218,7 +224,7 @@ struct pack_indexing static_assert(I < sizeof...(Ts)); using type = decltype(detail::do_pack_indexing>::select( static_cast*>(nullptr)... - )); + ))::type; }; template @@ -246,7 +252,7 @@ struct at_c> static_assert(I < sizeof...(Ts)); using type = decltype(detail::do_pack_indexing>::select( static_cast*>(nullptr)... - )); + ))::type; }; #endif