Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #580 by using a fixed-point implementation for unit conversions using integer representations #615

Draft
wants to merge 39 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
cf9c05f
wip
burnpanck Sep 15, 2024
3635260
disabled two tests which now trigger #614
burnpanck Sep 15, 2024
74f30da
again; fix C++20 compatibility
burnpanck Sep 15, 2024
2f54c76
addessed most review concerns, fixed CI failure
burnpanck Sep 16, 2024
e003a58
fixed and expanded double_width_int implemenation, tried to fix a bug…
burnpanck Sep 16, 2024
60c94da
one more try
burnpanck Sep 16, 2024
d00b330
fixed pedantic error
burnpanck Sep 16, 2024
99d4315
Merge remote-tracking branch 'upstream/master' into feature/fixed-poi…
burnpanck Nov 5, 2024
653d3d2
fix formatting issues
burnpanck Nov 5, 2024
ed2574f
allow use of __(u)int128, and always use std::bit_width and friends
burnpanck Nov 5, 2024
e688ffc
silence pedantic warning about __int128
burnpanck Nov 5, 2024
55d8fd6
cross-platform silencing of pedantic warning
burnpanck Nov 5, 2024
38dcf64
Merge remote-tracking branch 'upstream/master' into feature/fixed-poi…
burnpanck Nov 6, 2024
ad76149
Apply suggestions from code review
burnpanck Nov 6, 2024
95cc9f3
more review-requested changes, good test-coverage of double_width_int…
burnpanck Nov 6, 2024
5f8eb5c
made hi_ and lo_ private members of double_width_int
burnpanck Nov 6, 2024
1b57404
attempt to fix tests on apple clang
burnpanck Nov 6, 2024
f673619
try to work around issues around friend instantiations of double_widt…
burnpanck Nov 6, 2024
f642d37
fix: gcc-12 friend compilation issue workaround
mpusz Nov 9, 2024
b6a6752
implement dedicated facilities to customise scaling of numbers with m…
burnpanck Nov 10, 2024
647ce6b
fixed a few more details
burnpanck Nov 10, 2024
464ecd4
Merge remote-tracking branch 'upstream/master' into feature/fixed-poi…
burnpanck Nov 10, 2024
e933be7
fix a few issues uncovered in CI
burnpanck Nov 11, 2024
6873c8b
fix formatting
burnpanck Nov 11, 2024
65a0ee4
fix module exports - does not yet inlude other review input
burnpanck Nov 11, 2024
0c1971e
addressed most review input
burnpanck Nov 11, 2024
4ef0210
fix includes (and use curly braces for constructor calls in measurmen…
burnpanck Nov 12, 2024
35ed472
first attempt at generating sparse CI run matrix in python; also, can…
burnpanck Nov 12, 2024
329b9f5
Merge branch 'master' into feature/faster-CI
burnpanck Nov 12, 2024
7fa15d2
fix formatting
burnpanck Nov 12, 2024
e464677
don't test Clang 19 just yet; fix cancel-in-progres
burnpanck Nov 12, 2024
cc9ea9d
add cancel-in-progress to all workflows
burnpanck Nov 12, 2024
a51462c
missing checkout in generate-matrix step
burnpanck Nov 12, 2024
f4c8e90
fix boolean conan options in dynamic CI matrix
burnpanck Nov 12, 2024
01f44c6
heed github warning, and use output file instead of set-output comman…
burnpanck Nov 12, 2024
5713243
fix clang 16
burnpanck Nov 12, 2024
ff11878
exclude clang18+debug from freestanding again
burnpanck Nov 12, 2024
b35e241
fix clang on macos-14 (arm64)
burnpanck Nov 12, 2024
ef0e7b3
Merge branch 'feature/faster-CI' into feature/fixed-point-multiplicat…
burnpanck Nov 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 228 additions & 0 deletions src/core/include/mp-units/bits/fixed_point.h
mpusz marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// The MIT License (MIT)
//
// Copyright (c) 2018 Mateusz Pusz
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

#pragma once

// IWYU pragma: private, include <mp-units/framework.h>
#include <mp-units/framework/magnitude.h>

#ifndef MP_UNITS_IN_MODULE_INTERFACE
#ifdef MP_UNITS_IMPORT_STD
import std;
#else
#include <bit>
#include <concepts>
#include <cstdint>
#include <cstdlib>
#include <limits>
#include <numbers>
#endif
#endif

namespace mp_units {
namespace detail {
burnpanck marked this conversation as resolved.
Show resolved Hide resolved

// this class synthesizes a double-width integer from two base-width integers.
template<std::integral T>
struct double_width_int {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I am not wrong, this class only works at compile-time. If that is the case, then all its interfaces should be consteval. If some older compilers will complain, then we have an MP_UNITS_CONSTEVAL workaround.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the arithmetic operators are needed at runtime for the fixed-point scaling (scaling an i64 with a constexpr fixed-point i64.64 is implemented as a multiplication by an i128 followed by a right-shift by 64 bit). There is a bit of freedom in what rounding behaviour we'd like to have, which would require support for some runtime addition/subtraction as-well.

On the other hand, constructors could be made consteval for the fixed-point use-case. That said, I was just increasing test coverage for the arithmetic operators, which I am currently writing as runtime tests. The goal is to test a significant number of value combinations, and while this could be done at compile-time, I fear for the compilation time :-).

static constexpr bool is_signed = std::is_signed_v<T>;
static constexpr std::size_t base_width = std::numeric_limits<std::make_unsigned_t<T>>::digits;
static constexpr std::size_t width = 2 * base_width;

using Th = T;
using Tl = std::make_unsigned_t<T>;

constexpr double_width_int() = default;
constexpr double_width_int(const double_width_int&) = default;

constexpr double_width_int& operator=(const double_width_int&) = default;
burnpanck marked this conversation as resolved.
Show resolved Hide resolved

constexpr double_width_int(Th hi_, Tl lo_) : hi(hi_), lo(lo_) {}

explicit(true) constexpr double_width_int(long double v)
burnpanck marked this conversation as resolved.
Show resolved Hide resolved
{
constexpr auto scale = int_power<long double>(2, base_width);
constexpr auto iscale = 1.l / scale;
hi = static_cast<Th>(v * iscale);
lo = static_cast<Tl>(v - (hi * scale));
}
template<std::integral U>
requires(is_signed || !std::is_signed_v<U>)
explicit(false) constexpr double_width_int(U v)
{
if constexpr (is_signed) {
hi = v < 0 ? Th{-1} : Th{0};
} else {
hi = 0;
}
lo = static_cast<Tl>(v);
}

explicit(true) constexpr operator Th() const { return static_cast<Th>(lo); }
burnpanck marked this conversation as resolved.
Show resolved Hide resolved

constexpr auto operator<=>(const double_width_int&) const = default;
burnpanck marked this conversation as resolved.
Show resolved Hide resolved

// calculates the double-width product of two base-size integers
static constexpr double_width_int wide_product_of(Th lhs, Tl rhs)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a factory function? If so, shouldn't the constructors be private?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a factory function, but note that it is not equivalent to the constructor taking a (hi, lo) pair. wide_product_of(lhs,rhs) creates an instance representing lhs * rhs, while double_width_int(hi,lo) creates an instance representing (hi<<base_width) + lo.

One could argue though that there is a certain risk of confusion about the semantics of the two-argument constructor, so we may want to still make it private and instead expose a factory function from_nibbles or similar.

{
constexpr std::size_t half_width = base_width / 2;
constexpr Tl msk = Tl(1) << half_width;
Th l1 = lhs >> half_width;
Tl l0 = static_cast<Tl>(lhs) & msk;
Tl r1 = rhs >> half_width;
Tl r0 = rhs & msk;
Tl t00 = l0 * r0;
Tl t01 = l0 * r1;
Th t10 = l1 * r0;
Th t11 = l1 * r1;
Tl m = (t01 & msk) + (static_cast<Tl>(t10) & msk) + (t00 >> half_width);
Th o1 = t11 + (m >> half_width) + (t10 >> half_width) + static_cast<Th>(t01 >> half_width);
Tl o0 = (t00 & msk) | ((m & msk) << half_width);
return {o1, o0};
}

template<std::integral Lhs>
friend constexpr auto operator*(Lhs lhs, const double_width_int& rhs)
{
using ret_t = double_width_int<std::common_type_t<Lhs, Th>>;
auto ret = ret_t::wide_product_of(lhs, rhs.lo);
ret.hi += lhs * rhs.hi;
return ret;
};


constexpr double_width_int operator+(Tl rhs) const
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be a non-member two-parameter function to make sure that conversions work for lhs and rhs? Should there be another overload with reversed arguments?

{
auto ret = lo + rhs;
return {hi + (ret < lo ? 1 : 0), ret};
}

constexpr double_width_int operator>>(unsigned n) const
{
if (n >= base_width) {
return {0, hi >> (n - base_width)};
}
return {hi >> n, (static_cast<Tl>(hi) << (base_width - n)) | (lo >> n)};
}
constexpr double_width_int operator<<(unsigned n) const
{
if (n >= base_width) {
return {static_cast<Th>(lo >> (2 * base_width - n)), 0};
}
return {(hi << n) + static_cast<Th>(lo >> (base_width - n)), lo << n};
}

static constexpr double_width_int max() { return {std::numeric_limits<Th>::max(), std::numeric_limits<Tl>::max()}; }

Th hi;
Tl lo;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make those private? In case we do, can we name them with the _ postfix to be consistent with all other places in the library?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have strong feelings here. I was thinking it could be relevant for this type to be a literal type, given that we intend to use it mostly at compile-time. Perhaps it becomes useful in some numeric manipulation needed for magnitudes? Perhaps we want to store an offset between two quantity points/origins with 128 bits of precision? However, I'm aware that literal types are only needed if we want instances as NTTP, and neither of these use-cases definitely require a literal type. I'll make them private, we can still revert that later.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you meant structural type? litearal types are types that can be used in constexpr functions and those do not need public members.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, of course.

};

#if false && defined(__SIZEOF_INT128__)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always false?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, CI complains about the true-path because __int128_t is non-standard. Given that this is an implementation-detail, I should probably look for a way to disable that warning instead.

using int128_t = __int128;
using uint128_t = unsigned __int128;
inline constexpr std::size_t max_native_width = 128;
#else
using int128_t = double_width_int<std::int64_t>;
using uint128_t = double_width_int<std::uint64_t>;
inline constexpr std::size_t max_native_width = 64;
#endif

template<typename T>
inline constexpr std::size_t integer_rep_width_v = std::numeric_limits<std::make_unsigned_t<T>>::digits;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inline not needed from variable templates.

template<typename T>
inline constexpr std::size_t integer_rep_width_v<double_width_int<T>> = double_width_int<T>::width;

template<typename T>
inline constexpr bool is_signed_v = std::is_signed_v<T>;
template<typename T>
inline constexpr bool is_signed_v<double_width_int<T>> = double_width_int<T>::is_signed;

template<typename T>
using make_signed_t = std::make_signed_t<T>;

template<std::size_t N>
using min_width_uint_t =
std::tuple_element_t<std::max<std::size_t>(4u, std::bit_width(N) + (std::has_single_bit(N) ? 0u : 1u)) - 4u,
std::tuple<std::uint8_t, std::uint16_t, std::uint32_t, std::uint64_t, uint128_t>>;

static_assert(std::is_same_v<min_width_uint_t<1>, std::uint8_t>);
static_assert(std::is_same_v<min_width_uint_t<7>, std::uint8_t>);
static_assert(std::is_same_v<min_width_uint_t<8>, std::uint8_t>);
static_assert(std::is_same_v<min_width_uint_t<9>, std::uint16_t>);
static_assert(std::is_same_v<min_width_uint_t<31>, std::uint32_t>);
static_assert(std::is_same_v<min_width_uint_t<32>, std::uint32_t>);
static_assert(std::is_same_v<min_width_uint_t<33>, std::uint64_t>);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move unit tests to a dedicated unit test file. We do not want to obfuscate our production with unit tests and pay for them at compile-time.


template<std::size_t N>
using min_width_int_t = make_signed_t<min_width_uint_t<N>>;

template<typename T>
using double_width_int_for_t = std::conditional_t<is_signed_v<T>, min_width_int_t<integer_rep_width_v<T> * 2>,
min_width_uint_t<integer_rep_width_v<T> * 2>>;

template<typename Lhs, typename Rhs>
constexpr auto wide_product_of(Lhs lhs, Rhs rhs)
{
if constexpr (integer_rep_width_v<Lhs> + integer_rep_width_v<Rhs> <= max_native_width) {
using T = std::common_type_t<double_width_int_for_t<Lhs>, double_width_int_for_t<Rhs>>;
return static_cast<T>(lhs) * static_cast<T>(rhs);
} else {
using T = double_width_int<std::common_type_t<Lhs, Rhs>>;
return T::wide_product_of(lhs, rhs);
}
}

// This class represents rational numbers using a fixed-point representation, with a symmetric number of digits (bits)
// on either side of the decimal point. The template argument `T` specifies the range of the integral part,
// thus this class uses twice as many bits as the provided type, but is able to precisely store exactly all integers
// from the declared type, as well as efficiently describe all rational factors that can be applied to that type
// and neither always cause underflow or overflow.
template<std::integral T>
struct fixed_point {
using repr_t = double_width_int_for_t<T>;
burnpanck marked this conversation as resolved.
Show resolved Hide resolved
static constexpr std::size_t fractional_bits = integer_rep_width_v<T>;

constexpr fixed_point() = default;
constexpr fixed_point(const fixed_point&) = default;

constexpr fixed_point& operator=(const fixed_point&) = default;
burnpanck marked this conversation as resolved.
Show resolved Hide resolved

explicit constexpr fixed_point(long double v) :
int_repr_is_an_implementation_detail_(static_cast<repr_t>(v * int_power<long double>(2, fractional_bits)))
{
}

template<std::integral U>
requires(integer_rep_width_v<U> <= integer_rep_width_v<T>)
auto scale(U v) const
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constexpr?

{
auto res = v * int_repr_is_an_implementation_detail_;
return static_cast<std::conditional_t<is_signed_v<decltype((res))>, std::make_signed_t<U>, U>>(res >>
fractional_bits);
}

repr_t int_repr_is_an_implementation_detail_;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, literal type, but again, I don't actually need that right now. I'll make it private (... and remove the is_an_implementation_detail_). We can still change it if a need comes up.

};

} // namespace detail
} // namespace mp_units
109 changes: 69 additions & 40 deletions src/core/include/mp-units/bits/sudo_cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

#pragma once

#include <mp-units/bits/fixed_point.h>
#include <mp-units/ext/type_traits.h>
#include <mp-units/framework/magnitude.h>
#include <mp-units/framework/quantity_concepts.h>
Expand Down Expand Up @@ -53,14 +54,15 @@ template<Magnitude auto M, typename Rep1, typename Rep2>
struct conversion_type_traits {
using c_rep_type = maybe_common_type<Rep1, Rep2>;
using c_mag_type = common_magnitude_type<M>;
using multiplier_type = conditional<
treat_as_floating_point<c_rep_type>,
// ensure that the multiplier is also floating-point
conditional<std::is_arithmetic_v<value_type_t<c_rep_type>>,
// reuse user's type if possible
std::common_type_t<c_mag_type, value_type_t<c_rep_type>>, std::common_type_t<c_mag_type, double>>,
c_mag_type>;
using c_type = maybe_common_type<c_rep_type, multiplier_type>;
/* using multiplier_type = conditional<
treat_as_floating_point<c_rep_type>,
// ensure that the multiplier is also floating-point
conditional<std::is_arithmetic_v<value_type_t<c_rep_type>>,
// reuse user's type if possible
std::common_type_t<c_mag_type, value_type_t<c_rep_type>>, std::common_type_t<c_mag_type, double>>,
c_mag_type>;*/
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why to comment this code instead of removing it?

using c_type = conditional<std::is_arithmetic_v<value_type_t<c_rep_type>>, value_type_t<c_rep_type>,
std::common_type_t<c_mag_type, double>>;
};

/**
Expand All @@ -79,10 +81,56 @@ struct conversion_value_traits {
static constexpr Magnitude auto num = numerator(M);
static constexpr Magnitude auto den = denominator(M);
static constexpr Magnitude auto irr = M * (den / num);
static constexpr auto ratio = [] {
if constexpr (std::is_integral_v<T>) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about MyInt? Shouldn't it use fixed_point as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is MyInt?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean some user-defined wrapper type for an integral type. It will not satisfy std::integral but behaves like one. This is why I used treat_as_floating_point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right. The problem is that for fixed point types (including integers), we also need to know their range and/or bit width, and their "resolution" (1 for integers). The standard currently lacks a standard way to communicate those for numeric types, so we'd need to introduce further customisation points. Therefore, I currently believe it is better if just provide a general customisation point for scaling by a magnitude. If the custom type really is just a wrapper of an integer, they can just unwrap that integer, feed it back to our customisation point and then wrap the result again.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also use value_type_t to get and analyze the underlying representation type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I remember having seen that one in the sudo_cast implementation. How is value_type_t specified? Its implementation looks like std::type_identity, but I guess it is intended to act as a customisation point allowing to declare that a specific types T is? / has? / behaves like? / represents a? value of type value_type_t. A quick search however did not turn up any nontrivial use of it however.

using U = long double;
return detail::fixed_point<T>{get_value<U>(num) / get_value<U>(den) * get_value<U>(irr)};
} else {
return get_value<T>(num) / get_value<T>(den) * get_value<T>(irr);
}
}();
static constexpr bool value_increases = ratio >= T{1};

template<typename V>
static constexpr auto scale(V value)
{
if constexpr (std::is_integral_v<T>) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if constexpr (std::is_integral_v<T>) {
if constexpr (requires { ratio.scale(value); }) {

or

Suggested change
if constexpr (std::is_integral_v<T>) {
if constexpr (is_specialization_of<T, fixed_point>) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope. I don't think either of these suggestions are good choices here, but we may indeed want to refactor here. Right now, this std::is_integral_v<T> check really represents the treat_as_floating_point behavioural choice here. So this if here should really be replaced by a customisation point for custom representation types. I would decidedly not try to "guess" a scaling method here, but rather make it an explicit choice somewhere in the scaling machinery.

Instead, I believe I should refactor the conversion_value_traits a bit: In fact, these are really scaling_traits in the Scalable concept discussed recently. Right now, conversion_value_traits are separated by the properties of the scaling magnitude, but I believe this should be reversed: The "outer level" is the potentially customisable scaling behaviour of the representation types. The implementation of that scaling behaviour for different magnitude types is almost an implementation detail and thus should be at the "inner level".

return ratio.scale(value);
} else {
return value * ratio;
}
}
};

template<Magnitude auto M, typename T>
requires(is_integral(M))
struct conversion_value_traits<M, T> {
static constexpr Magnitude auto num = numerator(M);
static constexpr T num_mult = get_value<T>(num);
static constexpr T den_mult = get_value<T>(den);
static constexpr T irr_mult = get_value<T>(irr);
static constexpr T ratio = num_mult / den_mult * irr_mult;
static constexpr bool value_increases = true;

static_assert(get_value<T>(denominator(M)) == 1);
burnpanck marked this conversation as resolved.
Show resolved Hide resolved
static_assert(is_integral(M));
burnpanck marked this conversation as resolved.
Show resolved Hide resolved

template<typename V>
static constexpr auto scale(V value)
{
return value * num_mult;
}
};

template<Magnitude auto M, typename T>
requires(is_integral(pow<-1>(M)) && !is_integral(M))
struct conversion_value_traits<M, T> {
static constexpr Magnitude auto den = denominator(M);
static constexpr T den_div = get_value<T>(den);
static constexpr bool value_increases = false;

template<typename V>
static constexpr auto scale(V value)
{
return value / den_div;
}
};


Expand Down Expand Up @@ -110,30 +158,11 @@ template<Quantity To, typename FwdFrom, Quantity From = std::remove_cvref_t<FwdF
} else {
constexpr Magnitude auto c_mag = get_canonical_unit(From::unit).mag / get_canonical_unit(To::unit).mag;
using type_traits = conversion_type_traits<c_mag, typename From::rep, typename To::rep>;
using multiplier_type = typename type_traits::multiplier_type;
// TODO the below crashed nearly every compiler I tried it on
// auto scale = [&](std::invocable<typename type_traits::c_type> auto func) {
auto scale = [&](auto func) {
auto res =
static_cast<To::rep>(func(static_cast<type_traits::c_type>(q.numerical_value_is_an_implementation_detail_)));
return To{res, To::reference};
};

// scale the number
if constexpr (is_integral(c_mag))
return scale([&](auto value) { return value * get_value<multiplier_type>(numerator(c_mag)); });
else if constexpr (is_integral(pow<-1>(c_mag)))
return scale([&](auto value) { return value / get_value<multiplier_type>(denominator(c_mag)); });
else {
using value_traits = conversion_value_traits<c_mag, multiplier_type>;
if constexpr (std::is_floating_point_v<multiplier_type>)
// this results in great assembly
return scale([](auto value) { return value * value_traits::ratio; });
else
// this is slower but allows conversions like 2000 m -> 2 km without loosing data
return scale(
[](auto value) { return value * value_traits::num_mult / value_traits::den_mult * value_traits::irr_mult; });
}
using value_traits = conversion_value_traits<c_mag, typename type_traits::c_type>;

auto res = static_cast<To::rep>(
value_traits::scale(static_cast<type_traits::c_rep_type>(q.numerical_value_is_an_implementation_detail_)));
return To{res, To::reference};
}
}

Expand Down Expand Up @@ -172,21 +201,21 @@ template<QuantityPoint ToQP, typename FwdFromQP, QuantityPoint FromQP = std::rem
// and target unit and representation.
constexpr Magnitude auto c_mag = get_canonical_unit(FromQP::unit).mag / get_canonical_unit(ToQP::unit).mag;
using type_traits = conversion_type_traits<c_mag, typename FromQP::rep, typename ToQP::rep>;
using value_traits = conversion_value_traits<c_mag, typename type_traits::multiplier_type>;
using c_rep_type = typename type_traits::c_rep_type;
if constexpr (value_traits::num_mult * value_traits::irr_mult > value_traits::den_mult) {
using c_type = type_traits::c_type;
using value_traits = conversion_value_traits<c_mag, c_type>;
if constexpr (value_traits::value_increases) {
// original unit had a larger unit magnitude; if we first convert to the common representation but retain the
// unit, we obtain the largest possible range while not causing truncation of fractional values. This is optimal
// for the offset computation.
return sudo_cast<ToQP>(
sudo_cast<quantity_point<FromQP::reference, FromQP::point_origin, c_rep_type>>(std::forward<FwdFromQP>(qp))
sudo_cast<quantity_point<FromQP::reference, FromQP::point_origin, c_type>>(std::forward<FwdFromQP>(qp))
.point_for(ToQP::point_origin));
} else {
// new unit may have a larger unit magnitude; we first need to convert to the new unit (potentially causing
// truncation, but no more than if we did the conversion later), but make sure we keep the larger of the two
// representation types. Then, we can perform the offset computation.
return sudo_cast<ToQP>(
sudo_cast<quantity_point<make_reference(FromQP::quantity_spec, ToQP::unit), FromQP::point_origin, c_rep_type>>(
sudo_cast<quantity_point<make_reference(FromQP::quantity_spec, ToQP::unit), FromQP::point_origin, c_type>>(
std::forward<FwdFromQP>(qp))
.point_for(ToQP::point_origin));
}
Expand Down
3 changes: 2 additions & 1 deletion test/static/international_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ using namespace mp_units::international;
using namespace mp_units::international::unit_symbols;

// Mass
static_assert(100'000'000 * isq::mass[lb] == 45'359'237 * isq::mass[si::kilogram]);
// static_assert(100'000'000 * isq::mass[lb] == 45'359'237 * isq::mass[si::kilogram]);
// the previous test is currently disabled; it surfaced #614
static_assert(1 * isq::mass[lb] == 16 * isq::mass[oz]);
static_assert(1 * isq::mass[oz] == 16 * isq::mass[dr]);
static_assert(7'000 * isq::mass[gr] == 1 * isq::mass[lb]);
Expand Down
Loading
Loading