-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add overflow helpers for applying rational to int #212
Conversation
C++'s integer promotion rules for small types make this specific use case --- applying a rational magnitude to an integral type --- really interesting! The product type can be bigger than the type of the two inputs, so the overflow might not happen when you think it should. _Moreover,_ the fact that we subsequently divide by a denominator means that we can _re-enter_ the valid range and still produce a correct result! The only way I know how to deal with something this complicated is to make a separate target that does only that one thing. Happily, it's still the case that there is some maximum and minimum value for each type that will not overflow for a particular rational magnitude. The new helper target exists to figure out what those limits are. I exposed this limitation by adding a conversion between meters and yards in `uint16_t`. This is a great test case because the unit ratio between meters and yards is very close to 1 (it's 1143 to 1250), meaning the denominator division "rescues" the great majority of cases. Moreover, the multiplicative factor (either 1143 or 1250 depending on direction) is far less than the ratio between `uint16_t` and its promoted type, assuming the latter is equivalent to `int32_t`. So, despite the fact that we're multiplying by a number which is pretty large relative to the type, we almost never actually overflow! I fixed up the test case so that if it _does_ fail (say, we add some new unit conversion that exposes a new logic error), it will be very easy to understand why.
By the way: here's what we would get if we only applied the changes to
So, the current state of main calls certain yards-to-meters conversions lossy, even though the library produces a lossless result. With the present PR's changes, these tests all pass! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The production code looks better. The test coverage has improved around many tricky cases. Thanks!
constexpr auto huge = pow<400>(mag<10>()) / mag<3>(); | ||
|
||
{ | ||
int max_int = 123; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q: It looks like the value 123 is always written over. Is there any reason for this value vs 0?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope: I just needed something that is clearly not 0. 😅
au/apply_magnitude.hh
Outdated
@@ -141,13 +164,12 @@ struct ApplyMagnitudeImpl<Mag, ApplyAs::RATIONAL_MULTIPLY, T, true> { | |||
"Mismatched instantiation (should never be done manually)"); | |||
|
|||
constexpr T operator()(const T &x) { | |||
return x * get_value<T>(numerator(Mag{})) / get_value<T>(denominator(Mag{})); | |||
return static_cast<T>(x * get_value<T>(numerator(Mag{})) / |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Commend: Nice catch!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, in chatting about the logic with you, I noticed a bug, and it was here! Fortunately, it was pretty self-contained, and easy to write a test case for. Check out 81c4a3b for the fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! I was thinking about static_cast<T>
. I guess there's also the tricky bit about get_value<P>
. This line is trickier than it looks 😅
This is an edge case that I had missed before. The rational magnitude applier helper libraries all check whether `N` and `D` can fit in the _promoted_ type of `T`, not `T` itself. This is what they _should_ do, because this is the true limit of what's possible in the computation. However, on the _implementation_ side, we were getting the value in the type `T`. I was able to write a test case that exposed this, and now the test case passes. We should not need to worry about this for the other `ApplyMagnitude` specializations, because the `x * N / D` is the only case we have where the limits of the promoted type can produce a different outcome than the limits of the target type.
C++'s integer promotion rules for small types make this specific use
case --- applying a rational magnitude to an integral type --- really
interesting! The product type can be bigger than the type of the two
inputs, so the overflow might not happen when you think it should.
Moreover, the fact that we subsequently divide by a denominator means
that we can re-enter the valid range and still produce a correct
result!
The only way I know how to deal with something this complicated is to
make a separate target that does only that one thing. Happily, it's
still the case that there is some maximum and minimum value for each
type that will not overflow for a particular rational magnitude. The
new helper target exists to figure out what those limits are.
I exposed this limitation by adding a conversion between meters and
yards in
uint16_t
. This is a great test case because the unit ratiobetween meters and yards is very close to 1 (it's 1143 to 1250), meaning
the denominator division "rescues" the great majority of cases.
Moreover, the multiplicative factor (either 1143 or 1250 depending on
direction) is far less than the ratio between
uint16_t
and itspromoted type, assuming the latter is equivalent to
int32_t
. So,despite the fact that we're multiplying by a number which is pretty
large relative to the type, we almost never actually overflow!
I fixed up the test case so that if it does fail (say, we add some new
unit conversion that exposes a new logic error), it will be very easy to
understand why.