From 6d148e8007854193de7e049b93af231e010a0f89 Mon Sep 17 00:00:00 2001 From: Chip Hogg Date: Sun, 24 Mar 2024 16:14:22 -0400 Subject: [PATCH] Permit arithmetic operations with more types 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. --- au/BUILD.bazel | 25 +++++++ au/quantity.hh | 9 +-- au/quantity_test.cc | 32 +++++++++ au/rep.hh | 148 +++++++++++++++++++++++++++++++++++++++ au/rep_test.cc | 166 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 376 insertions(+), 4 deletions(-) create mode 100644 au/rep.hh create mode 100644 au/rep_test.cc diff --git a/au/BUILD.bazel b/au/BUILD.bazel index 7e945707..5843a97b 100644 --- a/au/BUILD.bazel +++ b/au/BUILD.bazel @@ -395,6 +395,7 @@ cc_library( ":apply_magnitude", ":conversion_policy", ":operators", + ":rep", ":unit_of_measure", ":zero", ], @@ -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([ diff --git a/au/quantity.hh b/au/quantity.hh index fb3752b9..b567d43a 100644 --- a/au/quantity.hh +++ b/au/quantity.hh @@ -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" @@ -293,21 +294,21 @@ class Quantity { } // Scalar multiplication. - template ::value>> + template ::value>> friend constexpr auto operator*(Quantity a, T s) { return make_quantity(a.value_ * s); } - template ::value>> + template ::value>> friend constexpr auto operator*(T s, Quantity a) { return make_quantity(s * a.value_); } // Scalar division. - template ::value>> + template ::value>> friend constexpr auto operator/(Quantity a, T s) { return make_quantity(a.value_ / s); } - template ::value>> + template ::value>> friend constexpr auto operator/(T s, Quantity a) { warn_if_integer_division(); return make_quantity(unit))>(s / a.value_); diff --git a/au/quantity_test.cc b/au/quantity_test.cc index 8e582333..733fa619 100644 --- a/au/quantity_test.cc +++ b/au/quantity_test.cc @@ -14,11 +14,14 @@ #include "au/quantity.hh" +#include + #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 { @@ -425,6 +428,35 @@ TEST(Quantity, ScalarMultiplicationWorks) { EXPECT_EQ(feet(9), d * 3); } +TEST(Quantity, SupportsMultiplicationForComplexRep) { + constexpr auto a = (miles / hour)(std::complex{1.0, -2.0}); + constexpr auto b = hours(std::complex{-3.0, 4.0}); + EXPECT_THAT(a * b, SameTypeAndValue(miles(std::complex{5.0, 10.0}))); +} + +TEST(Quantity, SupportsMultiplicationOfRealQuantityByComplexCoefficient) { + constexpr auto a = miles(10.0); + constexpr auto b = std::complex{-3.0, 4.0}; + EXPECT_THAT(a * b, SameTypeAndValue(miles(std::complex{-30.0, 40.0}))); + EXPECT_THAT(b * a, SameTypeAndValue(miles(std::complex{-30.0, 40.0}))); +} + +TEST(Quantity, SupportsDivisionOfRealQuantityByComplexCoefficient) { + constexpr auto a = miles(100.0); + constexpr auto b = std::complex{-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{-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.); diff --git a/au/rep.hh b/au/rep.hh new file mode 100644 index 00000000..562e8226 --- /dev/null +++ b/au/rep.hh @@ -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 + +#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 +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 +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 +struct IsQuotientValidRep; + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Implementation details below. +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// Forward declarations for main Au container types. +template +class Quantity; +template +class QuantityPoint; +template +struct CorrespondingQuantity; + +namespace detail { +template +struct IsAuType : std::false_type {}; + +template +struct IsAuType<::au::Quantity> : std::true_type {}; + +template +struct IsAuType<::au::QuantityPoint> : std::true_type {}; + +template +using CorrespondingUnit = typename CorrespondingQuantity::Unit; + +template +using CorrespondingRep = typename CorrespondingQuantity::Rep; + +template +struct HasCorrespondingQuantity + : stdx::conjunction, + stdx::experimental::is_detected> {}; + +template +using LooksLikeAuOrOtherQuantity = stdx::disjunction, HasCorrespondingQuantity>; + +// 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