sleep(300); |
Ile śpimy? 0.3s (300ms)? 5 minut (300s)? A może parametrem powyższego sleep jest jeszcze inna jednostka? Bez zajrzenia do dokumentacji – albo kodu – nie ma na to pytanie odpowiedzi. Akurat w przypadku jednostek czasu C++11 wprowadził zestaw klas odpowiedzialnych za nie, więc można napisać taki zupełnie jednoznaczny kod:
this_thread::sleep_for(chrono::seconds(30)); this_thread::sleep_for(500ms); // C++14 |
Ale jeśli chodzi o inne jednostki, np. długości, trzeba sobie radzić samemu. Pomocna będzie w tym klasa (a raczej szablon) std::ratio, który został również użyty dla typów w przestrzeni nazw std::chrono. Służy ona do reprezentacji prawie dowolnych liczb wymiernych. Jedynym ograniczeniem jest to, że zarówno mianownik jak i licznik muszą się mieścić w std::intmax_t oraz mianownik nie może być najniższą możliwe wartością swojego typu.
Wersja super prosta
Zacznijmy od utworzenia klasy reprezentującej wartość z mnożnikiem:
template<std::intmax_t Num, std::intmax_t Denom = 1, typename Type = double> struct ratioed_value { using type = ratioed_value; using value_type = Type; using ratio = std::ratio<Num, Denom>; explicit ratioed_value(value_type v): value{v} {} value_type count() const { return value; } private: value_type value; }; |
Wzorując się na chrono::duration_cast, napiszmy analogiczną funkcję dla ratioed_value:
template<typename To, typename From> To ratioed_value_cast(From&& from) { using divide = std::ratio_divide< typename std::remove_reference_t<From>::ratio, typename To::ratio >; return To{from.count() * divide::num / divide::den}; } |
Teraz wystarczy zdefiniować typy dla np. kilometrów i metrów:
using km = ratioed_value<1000>; using m = ratioed_value<1>; |
I można przystąpić do testów:
km len{2.5}; cout << ratioed_value_cast<m>(len).count(); // 2500 |
Usprawnienia
Powyższy szablon ma bardzo ograniczoną funkcjonalność. Na pewno można dodać mu domyślne konstruktory i operatory przypisania:
template<std::intmax_t Num, std::intmax_t Denom = 1, typename Type = double> struct ratioed_value { using type = ratioed_value; using value_type = Type; using ratio = std::ratio<Num, Denom>; explicit ratioed_value(value_type const& v): value{v} {} explicit ratioed_value(value_type&& v): value{std::move(v)} {} template<typename T, typename = typename std::remove_reference_t<T>::ratio> explicit ratioed_value(T&& t): ratioed_value{ ratioed_value_cast<ratioed_value>(std::forward<T>(t)) } {} ratioed_value(ratioed_value const& o) = default; ratioed_value(ratioed_value&& o) = default; ratioed_value& operator=(ratioed_value const&) = default; ratioed_value& operator=(ratioed_value&&) = default; template<typename T> ratioed_value& operator=(T&& t) { return *this = ratioed_value_cast<ratioed_value>(std::forward<T>(t)); } value_type count() const { return value; } private: value_type value; }; |
Można też pokusić się o implementację operatorów (mnożenie i dzielenie jest przez skalar):
ratioed_value operator-() const { return ratioed_value{value}; } template<typename T> ratioed_value& operator=(T&& t) { return *this = ratioed_value_cast<ratioed_value>(std::forward<T>(t)); } ratioed_value& operator+=(ratioed_value const& o) { value += o.value; return *this; } ratioed_value& operator-=(ratioed_value const& o) { value -= o.value; return *this; } ratioed_value& operator*=(value_type&& o) { value *= std::move(o); return *this; } ratioed_value& operator*=(value_type const& o) { value *= o; return *this; } friend ratioed_value operator*(ratioed_value l, value_type const& r) { return l *= r; } friend ratioed_value operator*(ratioed_value l, value_type&& r) { return l *= std::move(r); } friend ratioed_value operator*(value_type const& l, ratioed_value r) { return r *= l; } friend ratioed_value operator*(value_type&& l, ratioed_value r) { return r *= std::move(l); } ratioed_value& operator/=(value_type&& o) { value /= std::move(o); return *this; } ratioed_value& operator/=(value_type const& o) { value /= o; return *this; } friend ratioed_value operator/(ratioed_value l, value_type const& r) { return l /= r; } friend ratioed_value operator/(ratioed_value l, value_type&& r) { return l /= std::move(r); } friend ratioed_value operator/(value_type const& l, ratioed_value r) { return r /= l; } friend ratioed_value operator/(value_type&& l, ratioed_value r) { return r /= std::move(l); } bool operator==(ratioed_value const& o) const { return value == o.value; } bool operator==(ratioed_value&& o) const { return value == std::move(o.value); } bool operator!=(ratioed_value const& o) const { return *this != o; } bool operator!=(ratioed_value&& o) const { return *this != std::move(o); } bool operator<=(ratioed_value const& o) const { return value <= o.value; } bool operator<=(ratioed_value&& o) const { return value <= std::move(o.value); } bool operator>=(ratioed_value const& o) const { return value >= o.value; } bool operator>=(ratioed_value&& o) const { return value >= std::move(o.value); } bool operator<(ratioed_value const& o) const { return value < o.value; } bool operator<(ratioed_value&& o) const { return value < std::move(o.value); } bool operator>(ratioed_value const& o) const { return value > o.value; } bool operator>(ratioed_value&& o) const { return value > std::move(o.value); } |
Sporo tego? Owszem, ale najciekawsze dopiero przed nami – operatory dwuargumentowe. Celem jest umożliwienie zapisu
auto marathon = km{42} + m{195}; assert(marathon == m{42195}); |
Konieczny do tego jest dynamiczny wybór typu o większej precyzji (aby przypadkiem z powyższej sumy nie wyszło 42km). Rozwiązałem to w sposób następujący, ale może da się trochę ładniej:
namespace detail{ template<typename T, typename U> struct common_ratioed_value{}; template<std::intmax_t NumL, std::intmax_t DenomL, typename TypeL, std::intmax_t NumR, std::intmax_t DenomR, typename TypeR> struct common_ratioed_value< ratioed_value<NumL, DenomL, TypeL>, ratioed_value<NumR, DenomR, TypeR>> { using left = ratioed_value<NumL, DenomL, TypeL>; using right = ratioed_value<NumR, DenomR, TypeR>; using ratio = typename std::conditional_t< std::ratio_less<typename left::ratio, typename right::ratio>::value, left, right >::ratio; using value_type = std::common_type_t<TypeL, TypeR>; using type = ratioed_value<ratio::num, ratio::den, value_type>; }; } template<typename T, typename U> using common_ratioed_value = typename detail::common_ratioed_value< std::decay_t<T>, std::decay_t<U> >::type; template<typename T, typename U> auto operator+(T&& t, U&& u) -> common_ratioed_value<T, U> { using common = common_ratioed_value<T, U>; return ratioed_value_cast<common>(std::forward<T>(t)) += ratioed_value_cast<common>(std::forward<U>(u)); } template<typename T, typename U> auto operator-(T&& t, U&& u) -> common_ratioed_value<T, U> { using common = common_ratioed_value<T, U>; return ratioed_value_cast<common>(std::forward<T>(t)) -= ratioed_value_cast<common>(std::forward<U>(u)); } template<typename T, typename U> auto operator/(T&& t, U&& u) -> typename common_ratioed_value<T, U>::value_type { using common = common_ratioed_value<T, U>; return ratioed_value_cast<common>(std::forward<T>(t)).count() / ratioed_value_cast<common>(std::forward<U>(u)).count(); } template<typename T, typename U> using enable_bool_for_different_ratioed_values = std::enable_if_t< !std::is_same<std::decay_t<T>,std::decay_t<U>>::value, std::conditional_t< std::is_same<common_ratioed_value<T, U>, void>::value, bool, bool > >; template<typename T, typename U> auto operator==(T&& t, U&& u) -> enable_bool_for_different_ratioed_values<T, U> { using common = common_ratioed_value<T, U>; return ratioed_value_cast<common>(std::forward<T>(t)) == ratioed_value_cast<common>(std::forward<U>(u)); } template<typename T, typename U> auto operator!=(T&& t, U&& u) -> enable_bool_for_different_ratioed_values<T, U> { using common = common_ratioed_value<T, U>; return ratioed_value_cast<common>(std::forward<T>(t)) != ratioed_value_cast<common>(std::forward<U>(u)); } template<typename T, typename U> auto operator>=(T&& t, U&& u) -> enable_bool_for_different_ratioed_values<T, U> { using common = common_ratioed_value<T, U>; return ratioed_value_cast<common>(std::forward<T>(t)) >= ratioed_value_cast<common>(std::forward<U>(u)); } template<typename T, typename U> auto operator<=(T&& t, U&& u) -> enable_bool_for_different_ratioed_values<T, U> { using common = common_ratioed_value<T, U>; return ratioed_value_cast<common>(std::forward<T>(t)) <= ratioed_value_cast<common>(std::forward<U>(u)); } template<typename T, typename U> auto operator>(T&& t, U&& u) -> enable_bool_for_different_ratioed_values<T, U> { using common = common_ratioed_value<T, U>; return ratioed_value_cast<common>(std::forward<T>(t)) > ratioed_value_cast<common>(std::forward<U>(u)); } template<typename T, typename U> auto operator<(T&& t, U&& u) -> enable_bool_for_different_ratioed_values<T, U> { using common = common_ratioed_value<T, U>; return ratioed_value_cast<common>(std::forward<T>(t)) < ratioed_value_cast<common>(std::forward<U>(u)); } |
Rezultat
Korzystając z powyższego możemy teraz napawać się sukcesem:
template<std::intmax_t Num, std::intmax_t Denom = 1, typename T> ostream& operator<<(ostream& o, ratioed_value<Num, Denom, T> const& val) { o << "ratioed_value<" << Num; if(Denom != 1) o << ", " << Denom; o << "> {" << val.count() << "}"; return o; } int main() { using km = ratioed_value<1000, 1, long>; using m = ratioed_value<1, 1, long>; using light_second = ratioed_value<299792000>; using light_year = ratioed_value<9461000000000000>; // from WolframAlpha auto len = km{42} + m{195}; DBG(len); DBG(m{195} + km{42}); DBG(km{5} == 500 * m{10}); DBG(km{5} >= 500 * m{10}); DBG(km{5} > 500 * m{10}); DBG(light_year{1} / light_second{60 * 60 * 24}); DBG(km{42} == len); } |
Wyjście programu: [link]
len ratioed_value<1> {42195} m{195} + km{42} ratioed_value<1> {42195} km{5} == 500 * m{10} true km{5} >= 500 * m{10} true km{5} > 500 * m{10} false light_year{1} / light_second{60 * 60 * 24} 365.261 km{42} == len false |
O czym nie wspomniałem?
“Explicit is better than implicit”. W powyższym kodzie konwersje dokonywane są niejawnie, co utrudnia implementację. Ponadto w wielu przypadkach preferowane jest jawne wymuszanie zmiany jednostek, szczególnie gdy w przeciwnym wypadku efektem może być utracona precyzja.
Co dalej?
Jak łatwo zauważyć, o ile zamiana jednostek została “poprawiona” i taki kod pozwoliłby uniknąć problemu Gimli Glidera, ale poniższy kod wciąż skompiluje się i wykona bez błędu, mimo jego oczywistej błędności:
using m = ratioed_value<1, 1, long>; using km = ratioed_value<1000, 1, long>; using g = ratioed_value<1, 1, long>; using kg = ratioed_value<1000, 1, long>; kg weight = km{10}; |
Ale o tym w części 2.