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 1 commit
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
34 changes: 32 additions & 2 deletions example/measurement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,37 @@ class measurement {
[[nodiscard]] friend constexpr measurement operator+(const measurement& lhs, const measurement& rhs)
{
using namespace std;
return measurement(lhs.value() + rhs.value(), hypot(lhs.uncertainty(), rhs.uncertainty()));
return measurement{std::in_place, lhs.value() + rhs.value(), hypot(lhs.uncertainty(), rhs.uncertainty())};
}

[[nodiscard]] friend constexpr measurement operator-(const measurement& lhs, const measurement& rhs)
{
using namespace std;
return measurement(lhs.value() - rhs.value(), hypot(lhs.uncertainty(), rhs.uncertainty()));
return measurement{std::in_place, lhs.value() - rhs.value(), hypot(lhs.uncertainty(), rhs.uncertainty())};
}

template<typename To, mp_units::Magnitude M>
[[nodiscard]] constexpr measurement<To> scale(std::type_identity<measurement<To>>, M scaling_factor) const
Copy link
Owner

Choose a reason for hiding this comment

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

This type_identity usage here is quite novel. I understand the rationale, but maybe we can find a more traditional way of doing it? @JohelEGP, do you have any ideas here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea comes from boost::hana::type. Would be great if the standard had its own dedicated "type tag".

{
constexpr std::type_identity<To> to_value_type;
return measurement<To>{
std::in_place,
mp_units::scale(to_value_type, scaling_factor, value()),
mp_units::scale(to_value_type, scaling_factor, value()),
};
}

template<mp_units::Magnitude M>
[[nodiscard]] constexpr auto scale(M scaling_factor) const
{
return measurement{
std::in_place,
mp_units::scale(scaling_factor, value()),
mp_units::scale(scaling_factor, value()),
};
}


[[nodiscard]] friend constexpr measurement operator*(const measurement& lhs, const measurement& rhs)
{
const auto val = lhs.value() * rhs.value();
Expand Down Expand Up @@ -127,15 +149,23 @@ class measurement {
private:
value_type value_{};
value_type uncertainty_{};

// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
constexpr measurement(std::in_place_t, value_type val, value_type err) :
Copy link
Owner

Choose a reason for hiding this comment

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

This is not the right way of using in_place. in_place means that we pass a list of arguments for construction of one underlying type.

This is not the case here. We initialize two members with one argument each.

Copy link
Owner

Choose a reason for hiding this comment

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

If you would like to have in-place like construction for type then probably something like piecewise_construct is needed.

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, I just wanted a tag to select the private constructor, indicating "I know what I'm doing - I guarantee that err is positive". It's mostly a performance optimisation though, so, for an example, we could just leave it out completely.

Copy link
Owner

Choose a reason for hiding this comment

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

We can either remove it or use a new dedicated tag. Maybe something similar to:

inline constexpr struct validated_tag {
} validated;

value_(std::move(val)), uncertainty_(std::move(err))
{
}
};

} // namespace


template<typename T>
constexpr bool mp_units::is_scalar<measurement<T>> = true;
template<typename T>
constexpr bool mp_units::is_vector<measurement<T>> = true;

static_assert(mp_units::RepresentationOf<measurement<int>, mp_units::quantity_character::scalar>);
static_assert(mp_units::RepresentationOf<measurement<double>, mp_units::quantity_character::scalar>);

namespace {
Expand Down
1 change: 1 addition & 0 deletions src/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ add_mp_units_module(
include/mp-units/bits/module_macros.h
include/mp-units/bits/quantity_spec_hierarchy.h
include/mp-units/bits/ratio.h
include/mp-units/bits/scaling.h
include/mp-units/bits/sudo_cast.h
include/mp-units/bits/text_tools.h
include/mp-units/bits/type_list.h
Expand Down
238 changes: 238 additions & 0 deletions src/core/include/mp-units/bits/scaling.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// 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/bits/fixed_point.h>
#include <mp-units/framework/customization_points.h>

#ifndef MP_UNITS_IN_MODULE_INTERFACE
#ifdef MP_UNITS_IMPORT_STD
import std;
#else
#include <concepts>
#endif
#endif


namespace mp_units {

namespace detail {
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
namespace mp_units {
namespace detail {
namespace mp_units::detail {

The above is shorter and makes it harder to export public APIs from .../bits/....


template<std::floating_point A, std::floating_point B>
using minimal_floating_point_type =
std::conditional_t<(std::numeric_limits<A>::digits >= std::numeric_limits<B>::digits), A, B>;

template<typename To, typename T>
constexpr auto cast_integral(const T& value)
{
if constexpr (std::is_integral_v<T>) {
return static_cast<To>(value);
} else {
return value;
Copy link
Owner

Choose a reason for hiding this comment

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

If this is meant to be a floating point value, then !treat_as_floating_point should be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not really. The specification of cast_integral<To>(const From)& (an implementation detail) is "cast to To, if From is integral". The point is that the default implementation of scale basically has two separate cases:

  • "use operator*(float)": Applied whenever at least one of From and To is treat_as_floating_point.
  • "use a fixed-point multiplication": Can only be directly be applied to standard integral types, so is used just for those, plus custom types which are implicitly convertible and have an integral value_type.

So the first case is much broader. For standard types, this in particular includes the case where From is integer and To is floating-point. There, just multiplying the integer with some floating-point type may cause precision-loss warnigs to emitted. Those are "bogus", because whenever To is explicitly specified, we're in fact in an "explicit cast" path. So we need to do an explicit cast here. But on the other hand, we cannot unilaterally do an explicit cast, because many types that should go through that path are not at all convertible to that floating-point type (e.g. std::complex).

There are other ways to handle this. We could also just silence the warning locally, or branch-out the mixed int/float paths further above.

}
}

template<typename T>
struct floating_point_scaling_factor_type {
// fallback implementation for types with a `value_type` nested type
using type =
std::enable_if_t<!std::is_same_v<value_type_t<T>, T>, floating_point_scaling_factor_type<value_type_t<T>>>::type;
};
Copy link
Owner

Choose a reason for hiding this comment

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

Probably, it is better to leave the primary template undefined and handle value_type_t in a separate partial specialization. SFINAE is also much slower to compile than constraints and provides worse diagnostics.


template<std::floating_point T>
struct floating_point_scaling_factor_type<T> : std::type_identity<T> {};
Copy link
Owner

Choose a reason for hiding this comment

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

I know type_identity pattern, but it is actually more to type and requires one more dynamic allocation (class template instantiation) at compile time for each case. This is why I typically prefer to just type using.


template<std::integral T>
struct floating_point_scaling_factor_type<T> : std::common_type<T, float> {};

template<typename T>
requires requires { typename scaling_traits<T>::floating_point_scaling_factor_type; }
struct floating_point_scaling_factor_type<T> :
std::type_identity<typename scaling_traits<T>::floating_point_scaling_factor_type> {};


template<Magnitude auto M>
struct floating_point_scaling_impl {
static constexpr Magnitude auto num = _numerator(M);
static constexpr Magnitude auto den = _denominator(M);
static constexpr Magnitude auto irr = M * (den / num);
template<typename T>
static constexpr T ratio = [] {
using U = long double;
return static_cast<T>(_get_value<U>(M));
}();

template<typename To, typename From>
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
template<typename To, typename From>
template<std::floating_point To, std::floating_point From>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. floating_point_scaling_impl is not intended as "an implementation for floating point types" - it is "an implementation as multiplication with a floating-point scaling factor". As mentioned above, this includes cases where just one of the two is a floating-point type.

Copy link
Owner

Choose a reason for hiding this comment

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

In such a case, minimal_floating_point_type in the first line of its implementation should also not be constrained; otherwise, the code will not compile with other types.

static constexpr To scale(const From& value)
{
using U = minimal_floating_point_type<typename floating_point_scaling_factor_type<To>::type,
typename floating_point_scaling_factor_type<From>::type>;
if constexpr (_is_integral(M)) {
return static_cast<To>(cast_integral<U>(value) * _get_value<U>(num));
} else if constexpr (_is_integral(_pow<-1>(M))) {
return static_cast<To>(cast_integral<U>(value) / _get_value<U>(den));
} else {
return static_cast<To>(cast_integral<U>(value) * ratio<U>);
}
}
};

/**
* @brief Default implementation of `scaling_traits` for "floating-point like" types
*
* This class implements scaling by either multiplying or dividing the value with
* a floating-point representation of the scaling factor; the floating-point representation
* is chosen such that it is of comparable precision as the representation type,
*
* @note This is a low-level facility. Neither the `From`, nor the `To` types of the scaling
* operation are actually constrained to be floating-point or even "floating-point like" types.
* All it represents is the scaling operation as implemented by multiplication with a floating-point
* representation of the scaling factor. This is also used whenever simultaneously scaling and
* converting between integer and floating-point types.
*
* @tparam Rep Representation type
*/
template<typename Rep>
struct floating_point_scaling_traits {
template<Magnitude M, typename From>
static constexpr Rep scale_from(M, const From& value)
{
return floating_point_scaling_impl<M{}>::template scale<Rep>(value);
};

template<Magnitude M>
static constexpr auto scale(M, const Rep& value)
{
// for standard floating-point types, the result respresentation is always the same
return floating_point_scaling_impl<M{}>::template scale<Rep>(value);
}
};


template<Magnitude auto M>
struct fixed_point_scaling_impl {
static constexpr Magnitude auto num = _numerator(M);
static constexpr Magnitude auto den = _denominator(M);
template<std::integral T>
static constexpr auto ratio = [] {
using U = long double;
return detail::fixed_point<T>(_get_value<U>(M));
}();

template<typename To, typename From>
requires std::integral<value_type_t<To>> && std::integral<value_type_t<From>>
static constexpr To scale(const From& value)
{
using U = std::common_type_t<value_type_t<From>, value_type_t<To>>;
if constexpr (_is_integral(M)) {
return static_cast<To>(static_cast<value_type_t<From>>(value) * _get_value<U>(num));
} else if constexpr (_is_integral(_pow<-1>(M))) {
return static_cast<To>(static_cast<value_type_t<From>>(value) / _get_value<U>(den));
} else {
return static_cast<To>(ratio<U>.scale(static_cast<value_type_t<From>>(value)));
}
}
};


template<typename Rep>
struct fixed_point_scaling_traits {
template<Magnitude M, typename From>
static constexpr Rep scale_from(M, const From& value)
{
return fixed_point_scaling_impl<M{}>::template scale<Rep>(value);
};

template<Magnitude M>
static constexpr auto scale(M, const Rep& value)
{
// for standard integer types, the result respresentation is always the same
return fixed_point_scaling_impl<M{}>::template scale<Rep>(value);
}
};


template<typename T, typename Other = T>
inline constexpr auto select_scaling_traits = [] {
if constexpr (requires {
// we only check if the traits class is complete, not it's members; we do not want to fall-back
// depending on the argument types
{ sizeof(mp_units::scaling_traits<T>) } -> std::integral;
Copy link
Owner

Choose a reason for hiding this comment

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

Why not the following?

Suggested change
{ sizeof(mp_units::scaling_traits<T>) } -> std::integral;
{ typename mp_units::scaling_traits<T>; };

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'm not a standard lawyer, but my tests on godbolt suggest that the latter just tests if the type is declared, not if it is defined/complete.

Copy link
Owner

Choose a reason for hiding this comment

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

Right. Still, I am not sure if checking for size of gives us much more here, as this may be defined as just {} with no interface.

Anyway, checking the return type of sizeof probably is not needed here. Users should not declare functions with names of language keywords.

}) {
// traits class is defined; use that
return mp_units::scaling_traits<T>{};
Copy link
Owner

Choose a reason for hiding this comment

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

Shouldn't scaling traits also take Other? If our default implementation needs it, there are high chances that others may need it too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes and no. There are basically two cases:

  • We already know the desired result representation, perhaps because of a value_cast that includes a representation specification: Then, indeed we would like to choose the optimal scaling method for that pair of representation types. Currently, this functionality is customised through scaling_traits<To>::scale_from(Magnitude auto M, const From &from_value).
  • We don't know the desired result representation yet, perhaps because of a value_cast with just a unit, or as part of an addition involving different units: Then, some representations may be able to chose an optimal representation depending on the actual magnitude; e.g. an i22.10 fixed-point value counting meters would best be converted into an i12.20 fixed-point value counting kilometres. This functionality is currently customised through scaling_traits<From>::scale(Magnitude auto M, const From &from_value).

So, there are really two customisation points, one without specifying the output representation type, and one with. I do realise though that the one with explicit output type actually needs multi-dispatch, so the current approach is indeed not suitable. Perhaps we just need to select or create an appropriate placeholder for the To type to signal "unspecified" (perhaps void is good enough?). I'll give it a try.

} else {
// undefined traits class; fall-back to default handling
Copy link
Owner

Choose a reason for hiding this comment

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

Isn't it better to have dedicated specializations of scaling_traits for the following? With that, the above branch (and actually the entire select_scaling_traits will not be needed, and it could also provide some hints to the users on how to implement such things.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was my initial plan, actually. I pivoted as lead by the example of the quantity_like_traits, which are not defined for quantity either. I can try that approach.

Copy link
Owner

Choose a reason for hiding this comment

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

Well, I am not against having some implemention details here. But I think that in the PR we have too many layers for this: scale -> HasScalingTraits -> select_scaling_traits -> floating_point_scaling_traits -> floating_point_scaling_impl.

scale just using scaling_traits would be ideal. If scaling traits are meant only for users, then maybe scale itself should have a branch checking for scaling_traits<T>, and, if not provided, do a default implementation (like CPOs do).

if constexpr (mp_units::treat_as_floating_point<T> || mp_units::treat_as_floating_point<Other>) {
return floating_point_scaling_traits<T>{};
} else if constexpr (std::is_integral_v<T> ||
(std::is_integral_v<value_type_t<T>> && std::convertible_to<value_type_t<T>, T> &&
std::convertible_to<T, value_type_t<T>>)) {
return fixed_point_scaling_traits<value_type_t<T>>{};
} else {
// placeholder to report failure
return std::false_type{};
Copy link
Owner

Choose a reason for hiding this comment

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

This should be equivalent to the lack of definition for scaling_traits.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure I understand correctly: you suggest select_scaling_traits should not be defined for those types? Indeed, that is what I wanted, but somehow, I was unable to make it work. Let me try again, maybe it was due to another bug that I have fixed in the meantime...

Copy link
Owner

Choose a reason for hiding this comment

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

Yes, I think that having scaling_traits specialization for treat_as_floating_point and !treat_as_floating_point would be a better solution here. It would use interfaces that also the user has to use to make things like this work. We do not need any additional dispatch here.

In case of problems with constraints subsumption, we could expose proper concepts to make it work.

If there is no way to do it as above, then we could implement this in a similar way to CPOs (Niebloids).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The latest stint here now indeed works with a scaling_traits<From,To>, where To is optional. I provided an implementation for treat_as_floating_point (assuming that multiplication with a standard floating-point value is correct for all of those) and another one for "integer-likes" (standard integers, plus those which have a nested value_type that is a standard integer, and which further interconverts with that). Note that this does not cover all representation types (those which are neither treat_as_floating_point nor integer). That is on purpose. Any non-floating-point representation needs precise knowledge about range and precision, which we don't have. So requiring the user to provide a specialisation is the only right path here IMHO.

}
}
}();

template<typename T, typename Other = T>
concept HasScalingTraits = !std::convertible_to<decltype(select_scaling_traits<T, Other>), std::false_type>;

static_assert(HasScalingTraits<int>);
static_assert(HasScalingTraits<double>);

} // namespace detail

template<typename To, Magnitude M, typename From>
requires detail::HasScalingTraits<To, From> ||
requires(const From& value) { value.scale(std::type_identity<To>{}, M{}); }
constexpr To scale(std::type_identity<To> to_type, M scaling_factor, const From& value)
{
if constexpr (requires {
{ value.scale(to_type, scaling_factor) } -> std::convertible_to<To>;
}) {
return value.scale(to_type, scaling_factor);
} else {
return detail::select_scaling_traits<To, From>.scale_from(scaling_factor, value);
}
}
Copy link
Owner

Choose a reason for hiding this comment

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

With the above suggestions, this will simplify to:

Suggested change
template<typename To, Magnitude M, typename From>
requires detail::HasScalingTraits<To, From> ||
requires(const From& value) { value.scale(std::type_identity<To>{}, M{}); }
constexpr To scale(std::type_identity<To> to_type, M scaling_factor, const From& value)
{
if constexpr (requires {
{ value.scale(to_type, scaling_factor) } -> std::convertible_to<To>;
}) {
return value.scale(to_type, scaling_factor);
} else {
return detail::select_scaling_traits<To, From>.scale_from(scaling_factor, value);
}
}
template<typename To, Magnitude M, typename From>
requires requires { typename scaling_traits<To, From>; } ||
requires(const From& value) { value.scale(std::type_identity<To>{}, M{}); }
constexpr To scale(std::type_identity<To> to_type, M scaling_factor, const From& value)
{
if constexpr (requires {
{ value.scale(to_type, scaling_factor) } -> std::convertible_to<To>;
}) {
return value.scale(to_type, scaling_factor);
} else {
return scaling_traits<To, From>.scale_from(scaling_factor, value);
}
}


template<Magnitude M, typename From>
requires detail::HasScalingTraits<From> || requires(const From& value) { value.scale(M{}); }
constexpr auto scale(M scaling_factor, const From& value)
{
if constexpr (requires { value.scale(scaling_factor); }) {
return value.scale(scaling_factor);
Copy link
Owner

Choose a reason for hiding this comment

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

I do not think that T::scale is a good idea. I do not know of any representation type that provides such a member function today. Also, requiring representation types to add it tightly couples them with mp-units, which is not good. I would strongly prefer to have some non-member or type_traits-based customization only.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fine with me. Explicit specialisations are sometimes a bit painful to do because of the namespaces. That said, there are plenty of precedents in the standard library. ADL is a bit more convenient, and modern approaches would go through a CPO. On the other hand, I would actually prefer not to dispatch on the magnitude type at all, because if a different implementation is chosen for different magnitudes by accident, that would be very confusing. Instead, all paths for different magnitudes should remain close and under the control of the same "customiser". Thus, I think I'll give it another shot just explicit specialisation of mp_units::scaling_traits<From, To=UnspecifiedRepresentation>.

Copy link
Owner

Choose a reason for hiding this comment

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

ADL is not a good choice here as someone may want to provide a customization for a third-party existing type, and this would require injecting some stuff into the third-party namespace, which may not be allowed, like in the case of std.

} else {
return detail::select_scaling_traits<From>.scale(scaling_factor, value);
}
}

static_assert(_is_integral(mag<299'792'458>));
static_assert(_get_value<int>(mag<299'792'458>) == 299'792'458);
static_assert(scale(mag<299'792'458>, 1) == 299'792'458);
static_assert(scale(std::type_identity<int>{}, mag<299'792'458>, 1) == 299'792'458);

} // namespace mp_units
Loading