I'm implementing compile time unit system, I am able to multiply different units together such that, for example:
Scalar<int, M_>{2} * Scalar<int, M_>{2} == Scalar<int, M_, M_>{4};
I want also to be able to do this:
Scalar<int, M_, M_>{4} / Scalar<int, M_>{2} == Scalar<int, M_>{2};
And I think a good place to start would be to group similar units to a template< typename T, int P> struct UnitPower
type, so that
Scalar<int, UnitPower<M_, 1>>{2} * Scalar<int, UnitPower<M_, 1>>{2} == Scalar<int, UnitPower<M_, 2>>{4};
and
Scalar<int, UnitPower<M_, 2>>{4} / Scalar<int, UnitPower<M_, 1>>{2} == Scalar<int, UnitPower<M_, 1>>{2};
This will come in handy for a more general case:
Scalar<double, UnitPower<M_, 1>, UnitPower<S_, -2>, UnitPower<G_, 1>>{70} / Scalar<double, UnitPower<M_, 1>, UnitPower<S_, -1>>{10} == Scalar<double, UnitPower<S_, -1>, UnitPower<G_, 1>>{7}
I would also need to make the operators agnostic to the order of these UnitPowers...
Here's the code so far:
struct M_;
struct S_;
struct Mps_;
template<typename T, int P>
struct UnitPower {};
template<typename T, int P, typename... R>
struct group_units {
//static constexpr type = ?
};
template <typename T, class... C>
struct Scalar
{
protected:
T value;
public:
constexpr explicit Scalar(const T value) : value(value) {}
template<typename U>
constexpr auto operator<=>(const Scalar<U, C...> rhs) {
return value <=> static_cast<U>(rhs.value);
}
template<typename U>
constexpr bool operator==(const Scalar<U, C...> rhs) const { return value == static_cast<T>(rhs); }
template<typename U, typename... D>
constexpr Scalar<std::common_type_t<T, U>, C..., D...> operator/(const Scalar<U, D...> rhs) const {
using V = std::common_type_t<T, U>;
return Scalar<V, C..., D...>{static_cast<V>(value) / static_cast<V>(rhs)};
}
template<typename U, typename... D>
constexpr Scalar<std::common_type_t<T, U>, C..., D...> operator*(const Scalar<U, D...> rhs) const {
using V = std::common_type_t<T, U>;
return Scalar<V, C..., D...>{static_cast<V>(value) * static_cast<V>(rhs)};
}
template<typename U>
constexpr std::common_type_t<T, U> operator/(const Scalar<U, C...> rhs) const {
using V = std::common_type_t<T, U>;
return static_cast<V>(value) / static_cast<V>(rhs);
}
template<typename U>
constexpr Scalar<std::common_type_t<T, U>, C...> operator/(const U rhs) const {
using V = std::common_type_t<T, U>;
return Scalar<V, C...>{static_cast<V>(value) / static_cast<V>(rhs)};
}
constexpr explicit operator T() const { return value; }
template<typename U>
constexpr explicit operator U() const { return static_cast<U>(value); }
};
template<typename T>
struct Meters : Scalar<T, M_> { using Scalar<T, M_>::Scalar; };
template<typename T>
struct Seconds : Scalar<T, S_> { using Scalar<T, S_>::Scalar; };
As you can see, the division operator is currently only defined for same unit (in the same order), and returns just a number (which is the correct return type in this case), or takes just a number, returning the Scalar with no unit modification, which is also the correct behavior.
The multiplication operator just appends the units, and I need it to group them first.
I've added the
template<int P, typename T>
struct UnitPower {};
template<int P, typename T, typename... R>
struct group_units {
//static constexpr type = ?
};
Part, but I don't really know how to go about it..
I'm also not sure how to make the operators unit order agnostic.
After learning how to group the units for the multiplication operator, the division operator would be similar to the multiplication with regards to units - just using negative powers for the right hand side.
So my question is two-fold:
- How to make the function unit order agnostic?
- How to group similar units into a
UnitPower<U, int>
structs, and omit units whose power is0
? (and decayScalar
to the underlying value type if all UnitPowers are omitted).
CodePudding user response:
Pretty challenging... Even though I only did some basic tests below code seems to work, at very least it should give you quite a number of hints how to solve the problem. There's yet pretty much potential for beautifying the code (even though got less with latest edit) and naming of my templates sure is anything else but optimal – but I leave that to you to fix... As a little compensation division is already supplied, too ;)
The idea is always based on the same fundamental principle: We need to recurse into the template arguments to make the type changes we need. Keeping that in mind it should be possible to understand the code below. If questions remain, feel free to leave a comment.
template <typename Unit, int>
struct UnitPower { };
template <typename T, typename ... Units>
struct Scalar
{
T value;
};
template <typename ... Units>
struct concat;
template <typename ... Units>
using concat_t = typename concat<Units...>::type;
template <typename U, typename ... Units>
struct concat<U, std::tuple<Units...>>
{
using type = std::tuple<U, Units...>;
};
template <typename ... UnitsX, typename ... UnitsY>
struct concat<std::tuple<UnitsX...>, std::tuple<UnitsY...>>
{
using type = std::tuple<UnitsX..., UnitsY...>;
};
template <typename ... Units>
struct powers;
template <typename ... Units>
using powers_t = typename powers<Units...>::type;
template <>
struct powers<>
{
using type = std::tuple<>;
};
template <typename U, typename ... Units>
struct powers<U, Units...>
{
using type = concat_t<UnitPower<U, 1>, powers_t<Units...>>;
};
template <typename U, int N, typename ... Units>
struct powers<UnitPower<U, N>, Units...>
{
using type = concat_t<UnitPower<U, N>, powers_t<Units...>>;
};
template <typename ... Units>
struct count;
template <typename ... Units>
using count_t = typename count<Units...>::type;
template <typename U, int P>
struct count<UnitPower<U, P>>
{
using type = UnitPower<U, P>;
};
template <typename U, int PX, int PY, typename ... Units>
struct count<UnitPower<U, PX>, UnitPower<U, PY>, Units...>
{
using type = count_t<UnitPower<U, PX PY>, Units...>;
};
template <typename UX, int PX, typename UY, int PY, typename ... Units>
struct count<UnitPower<UX, PX>, UnitPower<UY, PY>, Units...>
{
using type = count_t<UnitPower<UX, PX>, Units...>;
};
template < typename ... Units>
struct count<std::tuple<Units...>>
{
using type = count_t<Units...>;
};
template <typename ... Units>
struct remain;
template <typename ... Units>
using remain_t = typename remain<Units...>::type;
template <typename U, int P>
struct remain<UnitPower<U, P>>
{
using type = std::tuple<>;
};
template <typename U, int PX, int PY, typename ... Units>
struct remain<UnitPower<U, PX>, UnitPower<U, PY>, Units...>
{
using type = remain_t<UnitPower<U, PX>, Units...>;
};
template <typename UX, int PX, typename UY, int PY, typename ... Units>
struct remain<UnitPower<UX, PX>, UnitPower<UY, PY>, Units...>
{
using type = concat_t<
UnitPower<UY, PY>,
remain_t<UnitPower<UX, PX>, Units...>
>;
};
template < typename ... Units>
struct remain<std::tuple<Units...>>
{
using type = remain_t<Units...>;
};
template <typename ... Units>
struct combine;
template <typename ... Units>
using combine_t = typename combine<Units...>::type;
template <>
struct combine<>
{
using type = std::tuple<>;
};
template <typename U, int P>
struct combine<UnitPower<U, P>>
{
using type = std::tuple<UnitPower<U, P>>;
};
template <typename U, int P, typename ... Units>
struct combine<UnitPower<U, P>, Units...>
{
using type = concat_t<
count_t<UnitPower<U, P>, Units...>,
combine_t<remain_t<UnitPower<U, P>, Units...>>
>;
};
template <typename ... Units>
struct combine<std::tuple<Units...>>
{
using type = combine_t<Units...>;
};
template <typename ... Units>
struct normalize;
template <typename ... Units>
using normalize_t = typename normalize<Units...>::type;
template <>
struct normalize<>
{
using type = std::tuple<>;
};
template <typename U, typename ... Units>
struct normalize<UnitPower<U, 0>, Units...>
{
using type = normalize_t<Units...>;
};
template <typename U, typename ... Units>
struct normalize<UnitPower<U, 1>, Units...>
{
using type = concat_t<U, normalize_t<Units...>>;
};
template <typename U, int N, typename ... Units>
struct normalize<UnitPower<U, N>, Units...>
{
using type = concat_t<UnitPower<U, N>, normalize_t<Units...>>;
};
template <typename ... Units>
struct normalize<std::tuple<Units...>>
{
using type = normalize_t<Units...>;
};
template <typename T, typename ... Units>
struct scalar;
template <typename ... Units>
using scalar_t = typename scalar<Units...>::type;
template <typename T, typename ... Units>
struct scalar<T, std::tuple<Units...>>
{
using type = Scalar<T, Units...>;
};
template <typename ... T>
struct multiply;
template <typename ... T>
using multiply_t = typename multiply<T...>::type;
template <typename TX, typename TY, typename ... UnitsX, typename ... UnitsY>
struct multiply<TX, TY, std::tuple<UnitsX...>, std::tuple<UnitsY...>>
{
using type = scalar_t<
decltype(std::declval<TX>() * std::declval<TY>()),
normalize_t<combine_t<concat_t<
powers_t<UnitsX...>, powers_t<UnitsY...>
>>>
>;
};
template <typename TX, typename TY, typename ... UnitsX, typename ... UnitsY>
auto operator*(Scalar<TX, UnitsX...> x, Scalar<TY, UnitsY...> y)
-> multiply_t<TX, TY, std::tuple<UnitsX...>, std::tuple<UnitsY...>>
{
return {x.value * y.value};
}
template <typename ... Units>
struct negate;
template <typename ... Units>
using negate_t = typename negate<Units...>::type;
template <>
struct negate<>
{
using type = std::tuple<>;
};
template <typename U, int N, typename ... Units>
struct negate<UnitPower<U, N>, Units...>
{
using type = concat_t<UnitPower<U, -N>, negate_t<Units...>>;
};
template <typename ... Units>
struct negate<std::tuple<Units...>>
{
using type = negate_t<Units...>;
};
template <typename ... T>
struct divide;
template <typename ... T>
using divide_t = typename divide<T...>::type;
template <typename TX, typename TY, typename ... UnitsX, typename ... UnitsY>
struct divide<TX, TY, std::tuple<UnitsX...>, std::tuple<UnitsY...>>
{
using type = scalar_t<
decltype(std::declval<TX>() / std::declval<TY>()),
normalize_t<combine_t<concat_t<
powers_t<UnitsX...>, negate_t<powers_t<UnitsY...>>
>>>
>;
};
template <typename TX, typename TY, typename ... UnitsX, typename ... UnitsY>
auto operator/(Scalar<TX, UnitsX...> x, Scalar<TY, UnitsY...> y)
-> divide_t<TX, TY, std::tuple<UnitsX...>, std::tuple<UnitsY...>>
{
return {x.value / y.value};
}
CodePudding user response:
Here's what I came up with.
#include <tuple>
#include <type_traits>
template <typename T, typename Tuple>
struct remove_from_tuple;
template <typename T, typename Tuple>
using remove_from_tuple_t = typename remove_from_tuple<T, Tuple>::type;
template <typename T, typename... ElemT>
struct remove_from_tuple<T, std::tuple<ElemT...>> {
using type = decltype(std::tuple_cat(std::declval<
std::conditional_t<std::is_same_v<T, ElemT>, std::tuple<>, std::tuple<ElemT>>
>()...));
static constexpr std::size_t removed =
sizeof...(ElemT) - std::tuple_size<type>::value;
};
template <class Tuple1, class Tuple2>
struct is_tuple_permutation : std::false_type {};
template <class Tuple1, class Tuple2>
constexpr bool is_tuple_permutation_v = is_tuple_permutation<Tuple1, Tuple2>::value;
template <>
struct is_tuple_permutation<std::tuple<>, std::tuple<>>
: public std::true_type {};
template <typename T, typename... List1, typename Tuple2>
struct is_tuple_permutation<std::tuple<T, List1...>, Tuple2> {
private:
using remove1_t = remove_from_tuple<T, std::tuple<List1...>>;
using remove2_t = remove_from_tuple<T, Tuple2>;
public:
static constexpr bool value =
1 remove1_t::removed == remove2_t::removed &&
is_tuple_permutation_v<typename remove1_t::type, typename remove2_t::type>;
};
struct M_;
struct S_;
struct KG_;
template <class Tag, int P>
struct UnitPower {};
// Trait: UnitPower<Tag0, P0>, UnitPower<Tag1, P1>, ... -> Tag0
template <class... Units>
struct first_tag;
template <class... Units>
using first_tag_t = typename first_tag<Units...>::type;
template <class Tag0, int P0, class... Units>
struct first_tag<UnitPower<Tag0, P0>, Units...> {
using type = Tag0;
};
// Trait: Sum powers of all UnitPower with matching Tag in Units...;
// Put all Units... with different Tag in tuple remainder.
template <class Tag, class... Units>
struct collect_unit;
template <class Tag, class... Units>
constexpr int collect_unit_power = collect_unit<Tag, Units...>::power;
template <class Tag, class... Units>
using collect_unit_remainder_t = typename collect_unit<Tag, Units...>::remainder;
template <class Tag>
struct collect_unit<Tag> {
static constexpr int power = 0;
using remainder = std::tuple<>;
};
template <class Tag, int P0, class... Units>
struct collect_unit<Tag, UnitPower<Tag, P0>, Units...> {
static constexpr int power = P0 collect_unit_power<Tag, Units...>;
using remainder = collect_unit_remainder_t<Tag, Units...>;
};
template <class Tag, class Unit0, class... Units>
struct collect_unit<Tag, Unit0, Units...> {
static constexpr int power = collect_unit_power<Tag, Units...>;
using remainder = decltype(std::tuple_cat(
std::declval<std::tuple<Unit0>>(),
std::declval<collect_unit_remainder_t<Tag, Units...>>()));
};
// Trait: Combine any units with the same Tag.
template <class Tuple>
struct group_units;
template <class Tuple>
using group_units_t = typename group_units<Tuple>::type;
template <>
struct group_units<std::tuple<>> {
using type = std::tuple<>;
};
template <class... Units>
struct group_units<std::tuple<Units...>> {
private:
using Tag0 = first_tag_t<Units...>;
using collect_t = collect_unit<Tag0, Units...>;
public:
using type = decltype(std::tuple_cat(
std::declval<std::conditional_t<
collect_t::power != 0,
std::tuple<UnitPower<Tag0, collect_t::power>>,
std::tuple<>>>(),
std::declval<group_units_t<typename collect_t::remainder>>()));
};
template <typename T, class... Units>
class Scalar;
// Trait: Do two Scalars have the same underlying type and the same units
// in any order?
template <class S1, class S2>
struct Scalars_compatible : public std::false_type {};
template <class S1, class S2>
constexpr bool Scalars_compatible_v = Scalars_compatible<S1, S2>::value;
template <typename T1, class... Units1, typename T2, class... Units2>
struct Scalars_compatible<Scalar<T1, Units1...>, Scalar<T2, Units2...>>
: public std::bool_constant<is_tuple_permutation_v<
std::tuple<Units1...>, std::tuple<Units2...>>>
{};
template <typename T, class Tuple>
struct tuple_to_Scalar;
template <typename T, class Tuple>
using tuple_to_Scalar_t = typename tuple_to_Scalar<T, Tuple>::type;
template <typename T, class... Units>
struct tuple_to_Scalar<T, std::tuple<Units...>> {
using type = Scalar<T, Units...>;
};
template <class S1, class S2>
struct Scalar_product;
template <class S1, class S2>
using Scalar_product_t = typename Scalar_product<S1, S2>::type;
template <typename T1, class... Units1, typename T2, class... Units2>
struct Scalar_product<Scalar<T1, Units1...>, Scalar<T2, Units2...>> {
using type = tuple_to_Scalar_t<
std::common_type_t<T1, T2>,
group_units_t<std::tuple<Units1..., Units2...>>>;
};
template <class Unit>
struct invert_unit;
template <class Unit>
using invert_unit_t = typename invert_unit<Unit>::type;
template <class Tag, int P>
struct invert_unit<UnitPower<Tag, P>> {
using type = UnitPower<Tag, -P>;
};
template <class S1, class S2>
struct Scalar_quotient;
template <class S1, class S2>
using Scalar_quotient_t = typename Scalar_quotient<S1, S2>::type;
template <typename T1, class... Units1, typename T2, class... Units2>
struct Scalar_quotient<Scalar<T1, Units1...>, Scalar<T2, Units2...>> {
using type = tuple_to_Scalar_t<
std::common_type_t<T1, T2>,
group_units_t<std::tuple<Units1..., invert_unit_t<Units2>...>>>;
};
using Distance_t = Scalar<double, UnitPower<M_, 1>>;
using Time_t = Scalar<double, UnitPower<S_, 1>>;
using Speed_t = Scalar_quotient_t<Distance_t, Time_t>;
using Acceleration_t = Scalar_quotient_t<Speed_t, Time_t>;
using Mass_t = Scalar<double, UnitPower<KG_, 1>>;
using Energy_t = Scalar_product_t<Mass_t, Acceleration_t>;
static_assert(Scalars_compatible_v<Energy_t, Scalar<double, UnitPower<KG_, 1>, UnitPower<M_, 1>, UnitPower<S_, -2>>>);
static_assert(Scalars_compatible_v<Scalar_quotient_t<Speed_t, Acceleration_t>, Time_t>);
Note you'll probably want to restrict your operator
, operator==
, etc. to require Scalars_compatible_v
.