Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions au/compatibility/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ cc_test(
deps = [
":eigen",
"//au",
"//au:io",
"//au:testing",
"@eigen",
"@googletest//:gtest_main",
Expand Down
108 changes: 107 additions & 1 deletion au/compatibility/eigen_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,20 @@
#include <Eigen/Core>
#include <Eigen/Geometry>
#include <Eigen/LU>
#include <sstream>
#include <string>

#include "au/au.hh"
#include "au/io.hh"
#include "au/testing.hh"
#include "gtest/gtest.h"

namespace au {

struct Meters : UnitImpl<Length> {};
struct Meters : UnitImpl<Length> {
static constexpr const char label[] = "m";
};
constexpr const char Meters::label[];
constexpr auto meters = QuantityMaker<Meters>{};

struct Feet : decltype(Meters{} * mag<381>() / mag<1250>()) {};
Expand All @@ -38,6 +44,7 @@ using ::testing::Eq;
using ::testing::IsFalse;
using ::testing::IsTrue;
using ::testing::StaticAssertTypeEq;
using ::testing::StrEq;

TEST(EigenCompatibility, CanCreateQuantityOfVector3d) {
Eigen::Vector3d v(1.0, 2.0, 3.0);
Expand Down Expand Up @@ -809,4 +816,103 @@ TEST(EigenFreeFunctions, CastAcceptsExpressionTemplateInput) {
EXPECT_THAT(result.data_in(meters), SameTypeAndValue(Eigen::Vector3d(2.0, 4.0, 6.0)));
}

template <typename T>
std::string streamed(const T &x) {
std::ostringstream oss;
oss << x;
return oss.str();
}

TEST(EigenCompatibility, CanStreamVectorQuantity) {
const Eigen::Vector3d v{1.0, 2.0, 3.0};

EXPECT_THAT(streamed(meters(v)), StrEq(streamed(v) + " m"));
}

TEST(EigenCompatibility, CanStreamExpressionRepQuantity) {
const Eigen::Vector3d v{1.0, 2.0, 3.0};

EXPECT_THAT(streamed(eval(transpose(meters(v)))), StrEq(streamed(v.transpose()) + " m"));
}

//
// Unit symbols with Eigen reps.
//

TEST(EigenUnitSymbols, SymbolOnRightMakesQuantityFromVector) {
constexpr auto m = symbol_for(meters);

const auto q = Eigen::Vector3d{1.0, 2.0, 3.0} * m;

StaticAssertTypeEq<decltype(q), const Quantity<Meters, Eigen::Vector3d>>();
EXPECT_THAT(q.data_in(meters), Eq(Eigen::Vector3d(1.0, 2.0, 3.0)));
}

TEST(EigenUnitSymbols, SymbolOnLeftMakesQuantityFromVector) {
constexpr auto m = symbol_for(meters);

const auto q = m * Eigen::Vector3d{1.0, 2.0, 3.0};

StaticAssertTypeEq<decltype(q), const Quantity<Meters, Eigen::Vector3d>>();
EXPECT_THAT(q.data_in(meters), Eq(Eigen::Vector3d(1.0, 2.0, 3.0)));
}

TEST(EigenUnitSymbols, DividingVectorBySymbolMakesInverseUnit) {
constexpr auto s = symbol_for(secs);

const auto q = Eigen::Vector3d{1.0, 2.0, 3.0} / s;

StaticAssertTypeEq<decltype(q), const Quantity<UnitInverseT<Secs>, Eigen::Vector3d>>();
EXPECT_THAT(q.data_in(inverse(secs)), Eq(Eigen::Vector3d(1.0, 2.0, 3.0)));
}

TEST(EigenUnitSymbols, ComposedSymbolsMakeCompoundUnit) {
constexpr auto m = symbol_for(meters);
constexpr auto s = symbol_for(secs);

const auto v = Eigen::Vector3d{4.0, 5.0, 6.0} * m / s;

StaticAssertTypeEq<decltype(v), const Quantity<UnitQuotientT<Meters, Secs>, Eigen::Vector3d>>();
EXPECT_THAT(v.data_in(meters / sec), Eq(Eigen::Vector3d(4.0, 5.0, 6.0)));
}

TEST(EigenUnitSymbols, MatrixRepWorksToo) {
constexpr auto m = symbol_for(meters);

const auto q = Eigen::Matrix2d{{1.0, 2.0}, {3.0, 4.0}} * m;

StaticAssertTypeEq<decltype(q), const Quantity<Meters, Eigen::Matrix2d>>();
EXPECT_THAT(q.data_in(meters), Eq(Eigen::Matrix2d({{1.0, 2.0}, {3.0, 4.0}})));
}

TEST(EigenUnitSymbols, IntegralScalarWorks) {
constexpr auto m = symbol_for(meters);

const auto q = Eigen::Vector3i{1, 2, 3} * m;

StaticAssertTypeEq<decltype(q), const Quantity<Meters, Eigen::Vector3i>>();
EXPECT_THAT(q.data_in(meters), Eq(Eigen::Vector3i(1, 2, 3)));
}

TEST(EigenUnitSymbols, ResultComposesWithTheRestOfTheLibrary) {
constexpr auto m = symbol_for(meters);
constexpr auto s = symbol_for(secs);

const auto p0 = Eigen::Vector3d{1.0, 2.0, 3.0} * m;
const auto v = Eigen::Vector3d{4.0, 5.0, 6.0} * m / s;
const auto t = 2.0 * s;

EXPECT_THAT(eval(p0 + v * t).data_in(meters), Eq(Eigen::Vector3d(9.0, 12.0, 15.0)));
}

TEST(EigenUnitSymbols, ExpressionTemplateInputIsAcceptedAsRep) {
// An expression template names a `Scalar` too, so it qualifies -- with the usual lifetime risk.
constexpr auto m = symbol_for(meters);
const Eigen::Vector3d v{1.0, 2.0, 3.0};

const auto q = eval((v + v) * m);

EXPECT_THAT(q.data_in(meters), Eq(Eigen::Vector3d(2.0, 4.0, 6.0)));
}

} // namespace au
20 changes: 14 additions & 6 deletions au/io.hh
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,23 @@

namespace au {

namespace detail {
// Unary `+` promotes a char-like rep (e.g. `int8_t`), so that `<<` prints a number rather than a
// character. Not every rep has one --- an Eigen vector does not --- so promote only where we can.
template <typename T>
constexpr auto promote_for_streaming(const T &x, int) -> decltype(+x) {
return +x;
}
template <typename T>
constexpr const T &promote_for_streaming(const T &x, ...) {
return x;
}
} // namespace detail

// Streaming output support for Quantity types.
template <typename U, typename R>
std::ostream &operator<<(std::ostream &out, const Quantity<U, R> &q) {
// In the case that the Rep is a type that resolves to 'char' (e.g. int8_t),
// the << operator will match the implementation that takes a character
// literal. Using the unary + operator will trigger an integer promotion on
// the operand, which will then match an appropriate << operator that will
// output the integer representation.
out << +q.in(U{}) << " " << unit_label(U{});
out << detail::promote_for_streaming(q.in(U{}), 0) << " " << unit_label(U{});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's part of the detail API, so not critical, but the 0 for the second parameter is not-intuitive.

And really, the only type we're really worried about is the single byte integers which happen to be a char underneath, and char has this unfortunate ambiguity. I suppose there's no good way to select on that?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed on all points. If you happen to find a better way to do this, I'm all ears! 😅 (It's an open invitation, so keep it in mind.)

return out;
}

Expand Down
27 changes: 8 additions & 19 deletions au/quantity.hh
Original file line number Diff line number Diff line change
Expand Up @@ -792,19 +792,11 @@ AU_DEVICE_FUNC constexpr auto rep_cast(Zero z) {

namespace detail {

// A SFINAE helper that is the identity, but only if we think a type is a valid rep.
//
// For now, we are restricting this to arithmetic types. This doesn't mean they're the only reps we
// support; it just means they're the only reps we can _construct via this method_. Later on, we
// would like to have a well-defined concept that defines what is and is not an acceptable rep for
// our `Quantity`. Once we have that, we can simply constrain on that concept. For more on this
// idea, see: https://github.com/aurora-opensource/au/issues/52
struct NoTypeMember {};
template <typename T>
struct TypeIdentityIfLooksLikeValidRepImpl
: std::conditional_t<std::is_arithmetic<T>::value, stdx::type_identity<T>, NoTypeMember> {};
// The identity on `T`, but only for a `T` we will accept as a `Rep`. `IsValidRep` excludes our own
// units, quantities, and other monovalue types, which is what keeps these overloads from competing
// with the ones meant for those.
template <typename T>
using TypeIdentityIfLooksLikeValidRep = typename TypeIdentityIfLooksLikeValidRepImpl<T>::type;
using TypeIdentityIfValidRep = TypeIdentityIf<::au::IsValidRep, T>;

// The unit whose `Constant` corresponds to a bare `Magnitude`: a scaled version of the unitless
// unit.
Expand All @@ -824,32 +816,29 @@ using UnitForMagnitude = ComputeScaledUnit<UnitProduct<>, M>;
// (N * M), for number N and magnitude M.
template <typename T, typename... BPs>
AU_DEVICE_FUNC constexpr auto operator*(T x, Magnitude<BPs...>)
-> Quantity<detail::UnitForMagnitude<Magnitude<BPs...>>,
detail::TypeIdentityIfLooksLikeValidRep<T>> {
-> Quantity<detail::UnitForMagnitude<Magnitude<BPs...>>, detail::TypeIdentityIfValidRep<T>> {
return make_quantity<detail::UnitForMagnitude<Magnitude<BPs...>>>(x);
}

// (M * N), for number N and magnitude M.
template <typename T, typename... BPs>
AU_DEVICE_FUNC constexpr auto operator*(Magnitude<BPs...>, T x)
-> Quantity<detail::UnitForMagnitude<Magnitude<BPs...>>,
detail::TypeIdentityIfLooksLikeValidRep<T>> {
-> Quantity<detail::UnitForMagnitude<Magnitude<BPs...>>, detail::TypeIdentityIfValidRep<T>> {
return make_quantity<detail::UnitForMagnitude<Magnitude<BPs...>>>(x);
}

// (N / M), for number N and magnitude M.
template <typename T, typename... BPs>
AU_DEVICE_FUNC constexpr auto operator/(T x, Magnitude<BPs...>)
-> Quantity<detail::UnitForMagnitude<MagInverse<Magnitude<BPs...>>>,
detail::TypeIdentityIfLooksLikeValidRep<T>> {
detail::TypeIdentityIfValidRep<T>> {
return make_quantity<detail::UnitForMagnitude<MagInverse<Magnitude<BPs...>>>>(x);
}

// (M / N), for number N and magnitude M.
template <typename T, typename... BPs>
AU_DEVICE_FUNC constexpr auto operator/(Magnitude<BPs...>, T x)
-> Quantity<detail::UnitForMagnitude<Magnitude<BPs...>>,
detail::TypeIdentityIfLooksLikeValidRep<T>> {
-> Quantity<detail::UnitForMagnitude<Magnitude<BPs...>>, detail::TypeIdentityIfValidRep<T>> {
static_assert(!std::is_integral<T>::value,
"Dividing by an integer value disallowed: would almost always produce 0");
return make_quantity<detail::UnitForMagnitude<Magnitude<BPs...>>>(T{1} / x);
Expand Down
39 changes: 39 additions & 0 deletions au/unit_symbol_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,37 @@

#include "au/unit_symbol.hh"

#include <complex>
#include <type_traits>
#include <vector>

#include "au/stdx/experimental/is_detected.hh"
#include "au/testing.hh"
#include "au/units/meters.hh"
#include "au/units/seconds.hh"
#include "gtest/gtest.h"

using ::testing::IsFalse;
using ::testing::IsTrue;
using ::testing::StaticAssertTypeEq;

namespace au {
namespace {
constexpr auto m = symbol_for(meters);
constexpr auto s = symbol_for(seconds);

// Detects which types a symbol will, and will not, make a quantity out of.
template <typename T>
using TimesMeterSymbol = decltype(std::declval<T>() * m);

// A stand-in for an Eigen vector; `//au/compatibility:eigen_test` covers the real thing.
template <typename T>
struct FakeEigenVector {
using Scalar = T;
T data[3];
};

struct Empty {};
} // namespace

TEST(SymbolFor, TakesUnitSlot) {
Expand All @@ -51,6 +69,27 @@ TEST(SymbolFor, CanScaleByMagnitude) {
EXPECT_THAT(3.5f / u100_m, SameTypeAndValue(inverse(meters * mag<100>())(3.5f)));
}

TEST(SymbolFor, MakesQuantityFromAnyValidRep) {
EXPECT_THAT((stdx::experimental::is_detected<TimesMeterSymbol, FakeEigenVector<double>>{}),
IsTrue());
EXPECT_THAT((stdx::experimental::is_detected<TimesMeterSymbol, std::complex<double>>{}),
IsTrue());
}

TEST(SymbolFor, RefusesTypesThatCannotBeReps) {
EXPECT_THAT((stdx::experimental::is_detected<TimesMeterSymbol, Empty>{}), IsFalse());

// A container of *quantities* can never be a valid rep: using it as one would nest units.
EXPECT_THAT(
(stdx::experimental::is_detected<TimesMeterSymbol, std::vector<QuantityD<Meters>>>{}),
IsFalse());
}

TEST(SymbolFor, QuantityStillScalesRatherThanBecomingARep) {
// If a `Quantity` also looked like a valid rep, this would be ambiguous with `ScalesQuantity`.
EXPECT_THAT(seconds(3.0) * m, SameTypeAndValue((seconds * meters)(3.0)));
}

TEST(SymbolFor, CanApplyNamedPowerFunctions) {
StaticAssertTypeEq<decltype(squared(m)), decltype(m * m)>();
}
Expand Down
24 changes: 24 additions & 0 deletions au/utility/test/type_traits_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

#include "au/utility/type_traits.hh"

#include <type_traits>

#include "au/stdx/experimental/is_detected.hh"
#include "gmock/gmock.h"
#include "gtest/gtest.h"

Expand All @@ -34,6 +37,27 @@ TEST(Prepend, PrependsToPack) {
StaticAssertTypeEq<Prepend<Pack<double, char>, int>, Pack<int, double, char>>();
}

// An overload constrained the way the library constrains its own: on the return type, so that a
// failing `Condition` removes it from the overload set instead of erroring. Declared, never
// defined; we only ever ask whether a call to it would compile.
template <typename T>
auto only_for_integral(T) -> TypeIdentityIf<std::is_integral, T>;

template <typename T>
using CallOnlyForIntegral = decltype(only_for_integral(std::declval<T>()));

TEST(TypeIdentityIf, IsTheIdentityWhenTheConditionHolds) {
StaticAssertTypeEq<TypeIdentityIf<std::is_integral, int>, int>();
StaticAssertTypeEq<TypeIdentityIf<std::is_floating_point, double>, double>();
}

TEST(TypeIdentityIf, HasNoTypeMemberWhenTheConditionFails) {
// The point of the trait: naming it is a substitution failure rather than a hard error, so an
// overload constrained on it simply drops out of the overload set.
EXPECT_THAT((stdx::experimental::is_detected<CallOnlyForIntegral, int>{}), IsTrue());
EXPECT_THAT((stdx::experimental::is_detected<CallOnlyForIntegral, double>{}), IsFalse());
}

TEST(SameTypeIgnoringCvref, IgnoresCvrefQualifiers) {
EXPECT_THAT((SameTypeIgnoringCvref<int, int &>::value), IsTrue());
EXPECT_THAT((SameTypeIgnoringCvref<const int &&, volatile int>::value), IsTrue());
Expand Down
18 changes: 18 additions & 0 deletions au/utility/type_traits.hh
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ constexpr bool same_type_ignoring_cvref(T, U) {
template <typename... Ts>
struct AlwaysFalse : std::false_type {};

//
// `TypeIdentityIf<Condition, T>` is `T` when `Condition<T>` holds, and a substitution failure when
// it doesn't: a way to constrain an overload through its return type.
//
template <bool Condition, typename T>
struct TypeIdentityIfImpl;
template <template <typename> class Condition, typename T>
using TypeIdentityIf = typename TypeIdentityIfImpl<Condition<T>::value, T>::type;

template <typename R1, typename R2>
struct CommonTypeButPreserveIntSignednessImpl;
template <typename R1, typename R2>
Expand All @@ -71,6 +80,15 @@ using PromotedType = typename PromotedTypeImpl<T>::type;
// Implementation details below.
////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////
// `TypeIdentityIf` implementation.

struct NoTypeMember {};
template <bool Condition, typename T>
struct TypeIdentityIfImpl : NoTypeMember {};
template <typename T>
struct TypeIdentityIfImpl<true, T> : stdx::type_identity<T> {};

////////////////////////////////////////////////////////////////////////////////////////////////////
// `PrependImpl` implementation.

Expand Down
Loading
Loading