-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
5 changed files
with
376 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.