From a3fad06f4a3bb5d66b276a34dbd7e0219b35bdb3 Mon Sep 17 00:00:00 2001 From: Chip Hogg Date: Thu, 19 Dec 2024 17:48:02 -0500 Subject: [PATCH] Improve error message for `Quantity` in unit slot (#366) This is an easy mistake to make for users who haven't yet grasped the idioms of the library. Now, if they do this, they'll get a nice, readable error, which directs them to the new section of the troubleshooting doc (also added in this PR) that explains what is going wrong, and what to do about it. Fixes #327. --- au/code/au/quantity.hh | 16 ++ au/code/au/quantity_point.hh | 16 ++ au/error_examples.cc | 9 + docs/discussion/idioms/unit-slots.md | 10 + docs/troubleshooting.md | 277 +++++++++++++++++++++++++++ 5 files changed, 328 insertions(+) diff --git a/au/code/au/quantity.hh b/au/code/au/quantity.hh index 9515ab86..94330cd8 100644 --- a/au/code/au/quantity.hh +++ b/au/code/au/quantity.hh @@ -424,6 +424,22 @@ class Quantity { Rep value_{}; }; +// Give more readable error messages when passing `Quantity` to a unit slot. +template +struct AssociatedUnit> { + static_assert( + detail::AlwaysFalse::value, + "Can't pass `Quantity` to a unit slot (see: " + "https://aurora-opensource.github.io/au/main/troubleshooting/#quantity-to-unit-slot)"); +}; +template +struct AssociatedUnitForPoints> { + static_assert( + detail::AlwaysFalse::value, + "Can't pass `Quantity` to a unit slot for points (see: " + "https://aurora-opensource.github.io/au/main/troubleshooting/#quantity-to-unit-slot)"); +}; + //////////////////////////////////////////////////////////////////////////////////////////////////// // Machinery to explicitly unblock integer division. // diff --git a/au/code/au/quantity_point.hh b/au/code/au/quantity_point.hh index 356bbe3c..6d2e96ae 100644 --- a/au/code/au/quantity_point.hh +++ b/au/code/au/quantity_point.hh @@ -282,6 +282,22 @@ struct QuantityPointMaker { template struct AssociatedUnitForPoints> : stdx::type_identity {}; +// Provide nicer error messages when users try passing a `QuantityPoint` to a unit slot. +template +struct AssociatedUnit> { + static_assert( + detail::AlwaysFalse::value, + "Cannot pass QuantityPoint to a unit slot (see: " + "https://aurora-opensource.github.io/au/main/troubleshooting/#quantity-to-unit-slot)"); +}; +template +struct AssociatedUnitForPoints> { + static_assert( + detail::AlwaysFalse::value, + "Cannot pass QuantityPoint to a unit slot (see: " + "https://aurora-opensource.github.io/au/main/troubleshooting/#quantity-to-unit-slot)"); +}; + // Type trait to detect whether two QuantityPoint types are equivalent. // // In this library, QuantityPoint types are "equivalent" exactly when they use the same Rep, and are diff --git a/au/error_examples.cc b/au/error_examples.cc index 0d7f0f2e..684ef1c7 100644 --- a/au/error_examples.cc +++ b/au/error_examples.cc @@ -13,6 +13,7 @@ // limitations under the License. #include "au/au.hh" +#include "au/units/bytes.hh" #include "au/units/feet.hh" #include "au/units/hertz.hh" #include "au/units/hours.hh" @@ -69,6 +70,14 @@ void example_no_type_named_type_in_std_common_type() { meters(1) + seconds(1); } +//////////////////////////////////////////////////////////////////////////////////////////////////// +// SECTION: Can't pass `Quantity` to a unit slot + +void example_cant_pass_quantity_to_unit_slot() { + auto size = bytes(1234); + size = round_as(bytes(10), size); +} + //////////////////////////////////////////////////////////////////////////////////////////////////// // SECTION: Integer division forbidden diff --git a/docs/discussion/idioms/unit-slots.md b/docs/discussion/idioms/unit-slots.md index 13fb70a1..e37c122a 100644 --- a/docs/discussion/idioms/unit-slots.md +++ b/docs/discussion/idioms/unit-slots.md @@ -193,6 +193,16 @@ The reason we endorse the `QuantityMaker` overloads is because of the convention a new `QuantityMaker` on the fly, then this benefit vanishes. (This is why unit expressions are preferred for generic code.) +## What _doesn't_ fit in a unit slot? + +A `Quantity`! It can certainly be tempting, as in some ways a `Quantity` can "feel like" a unit. +However, the `Quantity` also has a _runtime value_ attached. By contrast, unit slots can only take +things that have a single, unambiguous value, known _at compile time_. + +Fortunately, if you make this mistake, you'll get a readable compiler error that directs you to [our +troubleshooting page](../../troubleshooting.md#quantity-to-unit-slot), so you can learn more about +why this isn't allowed, and what you can do to fix it. + ## Summary Many Au APIs have a "unit slot". These are designed for you to name the units explicitly at the diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index a5623ad2..2fb17d39 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -679,6 +679,283 @@ dimension. Then, figure out how to fix your expression so it has the right dime D:\a\au\au\au.hh(4498): note: you cannot create a reference to 'void' ``` +## Can't pass `Quantity` to a unit slot {#quantity-to-unit-slot} + +**Other variants:** + +- "Can't pass `QuantityPoint` to a unit slot" +- "Can't pass `Quantity` to a unit slot for points" +- "Can't pass `QuantityPoint` to a unit slot for points" + +**Meaning:** A [unit slot](./discussion/idioms/unit-slots.md) is an API that takes _any unit-named +type in the library_, and treats it as the associated unit. Besides simple unit types themselves, +these can include quantity makers (such as `meters`), unit symbols (such as `symbols::m`), constants +(such as `SPEED_OF_LIGHT`), and so on. + +Notably, what it _cannot_ include is a `Quantity` or `QuantityPoint`. Notice that all of the types +we mentioned above have a _completely unambiguous value_, known at compile time from the _type +alone_. This is not the case for something like `Quantity`, which holds an underlying runtime +numeric value, to represent the quantity in its specific unit. + +**Solution:** If you're attempting to use the `Quantity` as an ad hoc unit, simply replace it with +a unit that you scale by a magnitude, `mag()`. + +!!! example + + **Code** + + Let's try to round a quantity of bytes to the nearest 10-byte amount. + + === "Broken" + ```cpp + // (BROKEN): can't pass Quantity to unit slot. + auto size = bytes(1234); + size = round_as(bytes(10), size); + // unit slot ^^^^^^^^^ passing Quantity: no good. + ``` + + === "Fixed" + ```cpp + // (FIXED): use an ad hoc scaled unit. + auto size = bytes(1234); + size = round_as(bytes * mag<10>(), size); + // unit slot ^^^^^^^^^^^^^^^^^ passing scaled unit: good! + ``` + + **Compiler error (clang 14)** + + ``` + In file included from au/error_examples.cc:15: + In file included from au/code/au/au.hh:17: + In file included from au/code/au/chrono_interop.hh:20: + In file included from au/code/au/prefix.hh:18: + au/code/au/quantity.hh:430:5: error: static_assert failed due to requirement 'detail::AlwaysFalse::value' "Can't pass `Quantity` to a unit slot" + static_assert(detail::AlwaysFalse::value, "Can't pass `Quantity` to a unit slot"); + ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + au/code/au/unit_of_measure.hh:147:1: note: in instantiation of template class 'au::AssociatedUnit>' requested here + using AssociatedUnitT = typename AssociatedUnit::type; + ^ + au/code/au/math.hh:434:26: note: in instantiation of template type alias 'AssociatedUnitT' requested here + return make_quantity>(round_in(rounding_units, q)); + ^ + au/error_examples.cc:78:12: note: in instantiation of function template specialization 'au::round_as, au::Bytes, int>' requested here + size = round_as(bytes(10), size); + ^ + ``` + + **Compiler error (clang 11)** + + ``` + In file included from au/error_examples.cc:15: + In file included from au/code/au/au.hh:17: + In file included from au/code/au/chrono_interop.hh:20: + In file included from au/code/au/prefix.hh:18: + au/code/au/quantity.hh:430:5: error: static_assert failed due to requirement 'detail::AlwaysFalse::value' "Can't pass `Quantity` to a unit slot" + static_assert(detail::AlwaysFalse::value, "Can't pass `Quantity` to a unit slot"); + ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + au/code/au/unit_of_measure.hh:147:1: note: in instantiation of template class 'au::AssociatedUnit>' requested here + using AssociatedUnitT = typename AssociatedUnit::type; + ^ + au/code/au/math.hh:434:26: note: in instantiation of template type alias 'AssociatedUnitT' requested here + return make_quantity>(round_in(rounding_units, q)); + ^ + au/error_examples.cc:78:12: note: in instantiation of function template specialization 'au::round_as, au::Bytes, int>' requested here + size = round_as(bytes(10), size); + ^ + ``` + + **Compiler error (gcc 10)** + + ``` + au/code/au/quantity.hh: In instantiation of 'struct au::AssociatedUnit >': + au/code/au/unit_of_measure.hh:147:7: required by substitution of 'template using AssociatedUnitT = typename au::AssociatedUnit::type [with U = au::Quantity]' + au/code/au/math.hh:434:12: required from 'auto au::round_as(RoundingUnits, au::Quantity) [with OutputRep = int; RoundingUnits = au::Quantity; U = au::Bytes; R = int]' + au/error_examples.cc:78:41: required from here + au/code/au/quantity.hh:430:46: error: static assertion failed: Can't pass `Quantity` to a unit slot + 430 | static_assert(detail::AlwaysFalse::value, "Can't pass `Quantity` to a unit slot"); + | ^~~~~ + In file included from au/code/au/conversion_policy.hh:22, + from au/code/au/quantity.hh:20, + from au/code/au/prefix.hh:18, + from au/code/au/chrono_interop.hh:20, + from au/code/au/au.hh:17, + from au/error_examples.cc:15: + au/code/au/unit_of_measure.hh: In substitution of 'template using AssociatedUnitT = typename au::AssociatedUnit::type [with U = au::Quantity]': + au/code/au/math.hh:434:12: required from 'auto au::round_as(RoundingUnits, au::Quantity) [with OutputRep = int; RoundingUnits = au::Quantity; U = au::Bytes; R = int]' + au/error_examples.cc:78:41: required from here + au/code/au/unit_of_measure.hh:147:7: error: no type named 'type' in 'struct au::AssociatedUnit >' + 147 | using AssociatedUnitT = typename AssociatedUnit::type; + | ^~~~~~~~~~~~~~~ + In file included from au/code/au/au.hh:19, + from au/error_examples.cc:15: + au/code/au/math.hh: In instantiation of 'auto au::round_in(RoundingUnits, au::Quantity) [with RoundingUnits = au::Quantity; U = au::Bytes; R = int]': + au/code/au/math.hh:400:43: required from 'auto au::round_in(RoundingUnits, au::Quantity) [with OutputRep = int; RoundingUnits = au::Quantity; U = au::Bytes; R = int]' + au/code/au/math.hh:434:77: required from 'auto au::round_as(RoundingUnits, au::Quantity) [with OutputRep = int; RoundingUnits = au::Quantity; U = au::Bytes; R = int]' + au/error_examples.cc:78:41: required from here + au/code/au/math.hh:382:52: error: no matching function for call to 'au::Quantity::in(au::Quantity&)' + 382 | return std::round(q.template in(rounding_units)); + | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~ + In file included from au/code/au/prefix.hh:18, + from au/code/au/chrono_interop.hh:20, + from au/code/au/au.hh:17, + from au/error_examples.cc:15: + au/code/au/quantity.hh:174:22: note: candidate: 'template constexpr NewRep au::Quantity::in(NewUnit) const [with NewRep = NewRep; NewUnit = NewUnit; = ; UnitT = au::Bytes; RepT = int]' + 174 | constexpr NewRep in(NewUnit u) const { + | ^~ + au/code/au/quantity.hh:174:22: note: template argument deduction/substitution failed: + au/code/au/quantity.hh:173:15: error: no type named 'type' in 'struct au::AssociatedUnit >' + 173 | typename = std::enable_if_t>::value>> + | ^~~~~~~~ + au/code/au/quantity.hh:184:19: note: candidate: 'template constexpr au::Quantity::Rep au::Quantity::in(NewUnit) const [with NewUnit = NewUnit; = ; UnitT = au::Bytes; RepT = int]' + 184 | constexpr Rep in(NewUnit u) const { + | ^~ + au/code/au/quantity.hh:184:19: note: template argument deduction/substitution failed: + In file included from external/sysroot_x86_64//include/c++/10.3.0/ratio:39, + from external/sysroot_x86_64//include/c++/10.3.0/chrono:39, + from au/code/au/chrono_interop.hh:17, + from au/code/au/au.hh:17, + from au/error_examples.cc:15: + external/sysroot_x86_64//include/c++/10.3.0/type_traits: In substitution of 'template using enable_if_t = typename std::enable_if::type [with bool _Cond = false; _Tp = void]': + au/code/au/quantity.hh:183:15: required from 'auto au::round_in(RoundingUnits, au::Quantity) [with RoundingUnits = au::Quantity; U = au::Bytes; R = int]' + au/code/au/math.hh:400:43: required from 'auto au::round_in(RoundingUnits, au::Quantity) [with OutputRep = int; RoundingUnits = au::Quantity; U = au::Bytes; R = int]' + au/code/au/math.hh:434:77: required from 'auto au::round_as(RoundingUnits, au::Quantity) [with OutputRep = int; RoundingUnits = au::Quantity; U = au::Bytes; R = int]' + au/error_examples.cc:78:41: required from here + external/sysroot_x86_64//include/c++/10.3.0/type_traits:2554:11: error: no type named 'type' in 'struct std::enable_if' + 2554 | using enable_if_t = typename enable_if<_Cond, _Tp>::type; + | ^~~~~~~~~~~ + In file included from au/code/au/prefix.hh:18, + from au/code/au/chrono_interop.hh:20, + from au/code/au/au.hh:17, + from au/error_examples.cc:15: + ``` + + **Compiler error (MSVC 2019 x64)** + + ``` + D:\a\au\au\au.hh(5267): error C2338: Can't pass `Quantity` to a unit slot (see: https://aurora-opensource.github.io/au/main/troubleshooting/#quantity-to-unit-slot) + D:\a\au\au\au.hh(7394): note: see reference to class template instantiation 'au::AssociatedUnit' being compiled + with + [ + RoundingUnits=au::Quantity + ] + D:\a\au\au\au.hh(7394): note: see reference to alias template instantiation 'au::AssociatedUnitT>' being compiled + error_examples.cc(79): note: see reference to function template instantiation 'auto au::round_as,au::Bytes,int>(RoundingUnits,au::Quantity)' being compiled + with + [ + RoundingUnits=au::Quantity + ] + D:\a\au\au\au.hh(3638): error C2794: 'type': is not a member of any direct or indirect base class of 'au::AssociatedUnit' + with + [ + RoundingUnits=au::Quantity + ] + D:\a\au\au\au.hh(7394): error C2938: 'au::AssociatedUnitT' : Failed to specialize alias template + D:\a\au\au\au.hh(7342): error C2672: 'au::Quantity::in': no matching overloaded function found + D:\a\au\au\au.hh(7360): note: see reference to function template instantiation 'auto au::round_in(RoundingUnits,au::Quantity)' being compiled + with + [ + RoundingUnits=au::Quantity + ] + D:\a\au\au\au.hh(7394): note: see reference to function template instantiation 'auto au::round_in(RoundingUnits,au::Quantity)' being compiled + with + [ + OutputRep=int, + RoundingUnits=au::Quantity + ] + D:\a\au\au\au.hh(7342): error C2783: 'NewRep au::Quantity::in(NewUnit) const': could not deduce template argument for '' + D:\a\au\au\au.hh(5010): note: see declaration of 'au::Quantity::in' + D:\a\au\au\au.hh(7360): error C2440: 'static_cast': cannot convert from 'void' to 'OutputRep' + with + [ + OutputRep=int + ] + D:\a\au\au\au.hh(7360): note: Expressions of type void cannot be converted to other types + D:\a\au\au\au.hh(7394): error C2672: 'au::make_quantity': no matching overloaded function found + D:\a\au\au\au.hh(7394): error C2893: Failed to specialize function template 'auto au::make_quantity(T)' + D:\a\au\au\au.hh(4872): note: see declaration of 'au::make_quantity' + D:\a\au\au\au.hh(7394): note: With the following template arguments: + D:\a\au\au\au.hh(7394): note: 'UnitT=unknown-type' + D:\a\au\au\au.hh(7394): note: 'T=void' + error_examples.cc(79): error C2679: binary '=': no operator found which takes a right-hand operand of type 'void' (or there is no acceptable conversion) + D:\a\au\au\au.hh(5261): note: could be 'au::Quantity &au::Quantity::operator =(au::Quantity &&)' + D:\a\au\au\au.hh(5261): note: or 'au::Quantity &au::Quantity::operator =(const au::Quantity &)' + error_examples.cc(79): note: while trying to match the argument list '(au::Quantity, void)' + ``` + + **Compiler error (MSVC 2022 x64)** + + ``` + D:\a\au\au\au.hh(5267): error C2338: static_assert failed: 'Can't pass `Quantity` to a unit slot (see: https://aurora-opensource.github.io/au/main/troubleshooting/#quantity-to-unit-slot)' + D:\a\au\au\au.hh(5267): note: the template instantiation context (the oldest one first) is + error_examples.cc(79): note: see reference to function template instantiation 'auto au::round_as,au::Bytes,int>(RoundingUnits,au::Quantity)' being compiled + with + [ + RoundingUnits=au::Quantity + ] + D:\a\au\au\au.hh(7394): note: see reference to alias template instantiation 'au::AssociatedUnitT' being compiled + with + [ + RoundingUnits=au::Quantity + ] + D:\a\au\au\au.hh(3638): note: see reference to class template instantiation 'au::AssociatedUnit' being compiled + with + [ + RoundingUnits=au::Quantity + ] + D:\a\au\au\au.hh(3638): error C2794: 'type': is not a member of any direct or indirect base class of 'au::AssociatedUnit' + with + [ + RoundingUnits=au::Quantity + ] + D:\a\au\au\au.hh(7394): error C2938: 'au::AssociatedUnitT' : Failed to specialize alias template + D:\a\au\au\au.hh(7342): error C2672: 'au::Quantity::in': no matching overloaded function found + D:\a\au\au\au.hh(5020): note: could be 'int au::Quantity::in(NewUnit) const' + D:\a\au\au\au.hh(7342): note: 'int au::Quantity::in(NewUnit) const': could not deduce template argument for '' + D:\a\au\au\au.hh(5019): note: 'std::enable_if_t' : Failed to specialize alias template + D:\a\au\au\au.hh(5010): note: or 'NewRep au::Quantity::in(NewUnit) const' + D:\a\au\au\au.hh(7342): note: 'NewRep au::Quantity::in(NewUnit) const': could not deduce template argument for '' + D:\a\au\au\au.hh(5009): note: 'au::AssociatedUnitT' : Failed to specialize alias template + D:\a\au\au\au.hh(3638): note: 'type': is not a member of any direct or indirect base class of 'au::AssociatedUnit' + with + [ + RoundingUnits=au::Quantity + ] + D:\a\au\au\au.hh(5009): note: syntax error: missing '>' before identifier '' + D:\a\au\au\au.hh(7342): note: the template instantiation context (the oldest one first) is + D:\a\au\au\au.hh(7394): note: see reference to function template instantiation 'auto au::round_in(RoundingUnits,au::Quantity)' being compiled + with + [ + OutputRep=int, + RoundingUnits=au::Quantity + ] + D:\a\au\au\au.hh(7360): note: see reference to function template instantiation 'auto au::round_in(RoundingUnits,au::Quantity)' being compiled + with + [ + RoundingUnits=au::Quantity + ] + D:\a\au\au\au.hh(7360): error C2440: 'static_cast': cannot convert from 'void' to 'OutputRep' + with + [ + OutputRep=int + ] + D:\a\au\au\au.hh(7360): note: Expressions of type void cannot be converted to other types + D:\a\au\au\au.hh(7394): error C2672: 'au::make_quantity': no matching overloaded function found + D:\a\au\au\au.hh(4872): note: could be 'auto au::make_quantity(T)' + D:\a\au\au\au.hh(7394): note: Failed to specialize function template 'auto au::make_quantity(T)' + D:\a\au\au\au.hh(7394): note: With the following template arguments: + D:\a\au\au\au.hh(7394): note: 'UnitT=unknown-type' + D:\a\au\au\au.hh(7394): note: 'T=void' + D:\a\au\au\au.hh(7394): note: 'void' cannot be used as a function parameter except for '(void)' + error_examples.cc(79): error C2679: binary '=': no operator found which takes a right-hand operand of type 'void' (or there is no acceptable conversion) + D:\a\au\au\au.hh(5261): note: could be 'au::Quantity &au::Quantity::operator =(au::Quantity &&)' + error_examples.cc(79): note: 'au::Quantity &au::Quantity::operator =(au::Quantity &&)': cannot convert argument 2 from 'void' to 'au::Quantity &&' + error_examples.cc(79): note: Expressions of type void cannot be converted to other types + D:\a\au\au\au.hh(5261): note: or 'au::Quantity &au::Quantity::operator =(const au::Quantity &)' + error_examples.cc(79): note: 'au::Quantity &au::Quantity::operator =(const au::Quantity &)': cannot convert argument 2 from 'void' to 'const au::Quantity &' + error_examples.cc(79): note: Expressions of type void cannot be converted to other types + error_examples.cc(79): note: while trying to match the argument list '(au::Quantity, void)' + ``` + ## Integer division forbidden {#integer-division-forbidden} **Meaning:** Although Au generally tries to act just like the underlying raw numeric types, we also