Skip to content

Commit

Permalink
Permit arithmetic operations with more types (#229)
Browse files Browse the repository at this point in the history
The goal of these `std::is_arithmetic` SFINAE uses was to avoid
ambiguous overloads when we multiply a `Quantity` with another
`Quantity`.  It was only ever a shortcut.  It seems to work well in most
cases, but recently, a user pointed out that it doesn't help
`std::complex`.  (To be clear, we could _form_ a `Quantity` with
`std::complex` rep, but we couldn't multiply or divide a `Quantity` with
a `std::complex` scalar.)

This PR takes a different approach.

First, we define what constitutes a valid "rep".  We're very permissive
here, using a "deny-list" approach that filters out empty types, known
Au types, and other libraries' units types (which we detect via our
`CorrespondingQuantity` trait).  Incidentally, this should give us a
nice head start on #52.

Next, we permit an operation (multiplication or division) depending on
how the "candidate scalar" type would interact with the Quantity's rep:
simply put, the result must itself both exist, and be a valid rep.

The net result should be that we automatically consider a much wider
variety of types --- including complex number types from outside the
standard library --- to be valid scalars.  Crucially, this should also
be able to avoid breaking existing code: we are introducing potentially
a _lot_ of new overloads for these operators, and we can't allow any to
create an ambiguity with existing overloads.

Helps #228, but we need more Magnitude support.
  • Loading branch information
chiphogg committed Apr 25, 2024
1 parent ffea274 commit 87bf5b3
Show file tree
Hide file tree
Showing 5 changed files with 376 additions and 4 deletions.
25 changes: 25 additions & 0 deletions au/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ cc_library(
":apply_magnitude",
":conversion_policy",
":operators",
":rep",
":unit_of_measure",
":zero",
],
Expand Down Expand Up @@ -438,6 +439,30 @@ cc_test(
],
)

cc_library(
name = "rep",
hdrs = ["rep.hh"],
deps = [":stdx"],
)

cc_test(
name = "rep_test",
size = "small",
srcs = ["rep_test.cc"],
deps = [
":chrono_interop",
":constant",
":magnitude",
":prefix",
":quantity",
":quantity_point",
":rep",
":unit_symbol",
":units",
"@com_google_googletest//:gtest_main",
],
)

cc_library(
name = "stdx",
srcs = glob([
Expand Down
9 changes: 5 additions & 4 deletions au/quantity.hh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "au/apply_magnitude.hh"
#include "au/conversion_policy.hh"
#include "au/operators.hh"
#include "au/rep.hh"
#include "au/stdx/functional.hh"
#include "au/unit_of_measure.hh"
#include "au/zero.hh"
Expand Down Expand Up @@ -293,21 +294,21 @@ class Quantity {
}

// Scalar multiplication.
template <typename T, typename = std::enable_if_t<std::is_arithmetic<T>::value>>
template <typename T, typename = std::enable_if_t<IsProductValidRep<RepT, T>::value>>
friend constexpr auto operator*(Quantity a, T s) {
return make_quantity<UnitT>(a.value_ * s);
}
template <typename T, typename = std::enable_if_t<std::is_arithmetic<T>::value>>
template <typename T, typename = std::enable_if_t<IsProductValidRep<T, RepT>::value>>
friend constexpr auto operator*(T s, Quantity a) {
return make_quantity<UnitT>(s * a.value_);
}

// Scalar division.
template <typename T, typename = std::enable_if_t<std::is_arithmetic<T>::value>>
template <typename T, typename = std::enable_if_t<IsQuotientValidRep<RepT, T>::value>>
friend constexpr auto operator/(Quantity a, T s) {
return make_quantity<UnitT>(a.value_ / s);
}
template <typename T, typename = std::enable_if_t<std::is_arithmetic<T>::value>>
template <typename T, typename = std::enable_if_t<IsQuotientValidRep<T, RepT>::value>>
friend constexpr auto operator/(T s, Quantity a) {
warn_if_integer_division<T>();
return make_quantity<decltype(pow<-1>(unit))>(s / a.value_);
Expand Down
32 changes: 32 additions & 0 deletions au/quantity_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@

#include "au/quantity.hh"

#include <complex>

#include "au/prefix.hh"
#include "au/testing.hh"
#include "au/utility/type_traits.hh"
#include "gtest/gtest.h"

using ::testing::DoubleEq;
using ::testing::StaticAssertTypeEq;

namespace au {
Expand Down Expand Up @@ -425,6 +428,35 @@ TEST(Quantity, ScalarMultiplicationWorks) {
EXPECT_EQ(feet(9), d * 3);
}

TEST(Quantity, SupportsMultiplicationForComplexRep) {
constexpr auto a = (miles / hour)(std::complex<double>{1.0, -2.0});
constexpr auto b = hours(std::complex<double>{-3.0, 4.0});
EXPECT_THAT(a * b, SameTypeAndValue(miles(std::complex<double>{5.0, 10.0})));
}

TEST(Quantity, SupportsMultiplicationOfRealQuantityByComplexCoefficient) {
constexpr auto a = miles(10.0);
constexpr auto b = std::complex<double>{-3.0, 4.0};
EXPECT_THAT(a * b, SameTypeAndValue(miles(std::complex<double>{-30.0, 40.0})));
EXPECT_THAT(b * a, SameTypeAndValue(miles(std::complex<double>{-30.0, 40.0})));
}

TEST(Quantity, SupportsDivisionOfRealQuantityByComplexCoefficient) {
constexpr auto a = miles(100.0);
constexpr auto b = std::complex<double>{-3.0, 4.0};
const auto quotient = (a / b).in(miles);
EXPECT_THAT(quotient.real(), DoubleEq(-12.0));
EXPECT_THAT(quotient.imag(), DoubleEq(-16.0));
}

TEST(Quantity, SupportsDivisionOfRealQuantityIntoComplexCoefficient) {
constexpr auto a = std::complex<double>{-30.0, 40.0};
constexpr auto b = miles(10.0);
const auto quotient = (a / b).in(inverse(miles));
EXPECT_THAT(quotient.real(), DoubleEq(-3.0));
EXPECT_THAT(quotient.imag(), DoubleEq(4.0));
}

TEST(Quantity, CanDivideArbitraryQuantities) {
constexpr auto d = feet(6.);
constexpr auto t = hours(3.);
Expand Down
148 changes: 148 additions & 0 deletions au/rep.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright 2024 Aurora Operations, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include <type_traits>

#include "au/stdx/experimental/is_detected.hh"
#include "au/stdx/type_traits.hh"

namespace au {

//
// A type trait that determines if a type is a valid representation type for `Quantity` or
// `QuantityPoint`.
//
template <typename T>
struct IsValidRep;

//
// A type trait to indicate whether the product of two types is a valid rep.
//
// Will validly return `false` if the product does not exist.
//
template <typename T, typename U>
struct IsProductValidRep;

//
// A type trait to indicate whether the quotient of two types is a valid rep.
//
// Will validly return `false` if the quotient does not exist.
//
template <typename T, typename U>
struct IsQuotientValidRep;

////////////////////////////////////////////////////////////////////////////////////////////////////
// Implementation details below.
////////////////////////////////////////////////////////////////////////////////////////////////////

// Forward declarations for main Au container types.
template <typename U, typename R>
class Quantity;
template <typename U, typename R>
class QuantityPoint;
template <typename T>
struct CorrespondingQuantity;

namespace detail {
template <typename T>
struct IsAuType : std::false_type {};

template <typename U, typename R>
struct IsAuType<::au::Quantity<U, R>> : std::true_type {};

template <typename U, typename R>
struct IsAuType<::au::QuantityPoint<U, R>> : std::true_type {};

template <typename T>
using CorrespondingUnit = typename CorrespondingQuantity<T>::Unit;

template <typename T>
using CorrespondingRep = typename CorrespondingQuantity<T>::Rep;

template <typename T>
struct HasCorrespondingQuantity
: stdx::conjunction<stdx::experimental::is_detected<CorrespondingUnit, T>,
stdx::experimental::is_detected<CorrespondingRep, T>> {};

template <typename T>
using LooksLikeAuOrOtherQuantity = stdx::disjunction<IsAuType<T>, HasCorrespondingQuantity<T>>;

// We need a way to form an "operation on non-quantity types only". That is: it's some operation,
// but _if either input is a quantity_, then we _don't even form the type_.
//
// The reason this very specific machinery lives in `rep.hh` is because when we're dealing with
// operations on "types that might be a rep", we know we can exclude quantity types right away.
// (Note that we're using the term "quantity" in an expansive sense, which includes not just
// `au::Quantity`, but also `au::QuantityPoint`, and "quantity-like" types from other libraries
// (which we consider as "anything that has a `CorrespondingQuantity`".
template <template <class...> class Op, typename... Ts>
struct ResultIfNoneAreQuantity;
template <template <class...> class Op, typename... Ts>
using ResultIfNoneAreQuantityT = typename ResultIfNoneAreQuantity<Op, Ts...>::type;

// Default implementation where we know that none are quantities.
template <bool AreAnyQuantity, template <class...> class Op, typename... Ts>
struct ResultIfNoneAreQuantityImpl : stdx::type_identity<Op<Ts...>> {};

// Implementation if any of the types are quantities.
template <template <class...> class Op, typename... Ts>
struct ResultIfNoneAreQuantityImpl<true, Op, Ts...> : stdx::type_identity<void> {};

// The main implementation.
template <template <class...> class Op, typename... Ts>
struct ResultIfNoneAreQuantity
: ResultIfNoneAreQuantityImpl<stdx::disjunction<LooksLikeAuOrOtherQuantity<Ts>...>::value,
Op,
Ts...> {};

// The `std::is_empty` is a good way to catch all of the various unit and other monovalue types in
// our library, which have little else in common. It's also just intrinsically true that it
// wouldn't make much sense to use an empty type as a rep.
template <typename T>
struct IsKnownInvalidRep
: stdx::disjunction<std::is_empty<T>, LooksLikeAuOrOtherQuantity<T>, std::is_same<void, T>> {};

// The type of the product of two types.
template <typename T, typename U>
using ProductType = decltype(std::declval<T>() * std::declval<U>());

template <typename T, typename U>
using ProductTypeOrVoid = stdx::experimental::detected_or_t<void, ProductType, T, U>;

// The type of the quotient of two types.
template <typename T, typename U>
using QuotientType = decltype(std::declval<T>() / std::declval<U>());

template <typename T, typename U>
using QuotientTypeOrVoid = stdx::experimental::detected_or_t<void, QuotientType, T, U>;
} // namespace detail

// Implementation for `IsValidRep`.
//
// For now, we'll accept anything that isn't explicitly known to be invalid. We may tighten this up
// later, but this seems like a reasonable starting point.
template <typename T>
struct IsValidRep : stdx::negation<detail::IsKnownInvalidRep<T>> {};

template <typename T, typename U>
struct IsProductValidRep
: IsValidRep<detail::ResultIfNoneAreQuantityT<detail::ProductTypeOrVoid, T, U>> {};

template <typename T, typename U>
struct IsQuotientValidRep
: IsValidRep<detail::ResultIfNoneAreQuantityT<detail::QuotientTypeOrVoid, T, U>> {};

} // namespace au

0 comments on commit 87bf5b3

Please sign in to comment.