Skip to content

Truncation

Truncation occurs when the result of a unit conversion lands in-between representable values in the destination type. Along with overflow, it’s one of the two main conversion risks in units libraries.

Au’s approach

Au implements unit conversions as sequences of operations. Some operations multiply or divide by a particular number. Others cast a value from one type to another.

Au checks every operation in the sequence to see whether any input values might cause truncation. If we find any risk, we forbid the conversion — at least, by default. There may be valid reasons to perform such a conversion anyway. If users are confident this is what they want, they can pass ignore(TRUNCATION_RISK) as a second argument to any conversion operator.

Usually, even conversions with truncation risk will truncate for only some values, and not for others. Users can check individual values at runtime to see if that particular value is safe. will_conversion_truncate(q, u) examines converting the specific Quantity value q to the new unit u. If the conversion being checked also changes the representation type (the “rep”), to a new type T, will_conversion_truncate<T>(q, u) is the right form to use.

These tools complement each other. A common pattern is to check an individual value at runtime, and then call the actual conversion function with ignore(TRUNCATION_RISK) if the check passes. Remember, the TRUNCATION_RISK flag refers to the risk of the conversion as a whole. If you’ve just proved that your particular value is safe, then ignoring that general risk is clearly fine.

Truncation in mathematical operations

By “mathematical operations”, we mainly mean multiplying or dividing by numerical constants. To understand the truncation risk, we first put the rep into one of several categories, because the risk profile strongly depends on the category.

The main categories are the arithmetic1 types: integral, and floating point. We’ll also briefly discuss how to think about other numeric types that we hope to support more fully in the future (#52).

Integral types

Integral types are considered exact. To see the implications for truncation, we need to consider various types of conversion factors.

First, we have multiplying by an integer. This is easy: it stays within the domain of the integers, so it can never truncate.

Next, we have multiplying by a non-integer rational (including reciprocal integers). This will truncate for any input that isn’t an exact integer multiple of the denominator, so the conversion as a whole clearly has truncation risk. When checking individual values, we can use the built-in % operator.

Finally, we have multiplication by an irrational number. For integral types, this truncates for every nonzero input, because integers are exact types, and the only integer that produces an integer when multiplied by an irrational is zero.

Floating point types

Mathematical operations on floating point types are governed by a very simple philosophy: floating point never truncates.

This may surprise the reader: after all, truncation means the result falls in-between representable values, and floating point types certainly have gaps! But consider the design philosophy of floating point. The goal is to emulate a continuous real line. Floating point values implicitly come with a relative tolerance: one that is small, but not zero. Each representable value effectively stands in for a small range of real values around it, and the exact size of that range depends on the precise calculations.

Or, more simply: floating point types are considered inexact.

Now, we return to the problem of truncation in units libraries. The very act of choosing a floating point representation is a statement, by the user, that exact values do not matter for their application. It would be inappropriate for us to raise warnings about perfectly routine properties of the user’s chosen type. Hence: floating point never truncates.

Compound types

Not every rep is a plain arithmetic type. Complex numbers and Eigen vectors/matrices are common examples of compound reps: types built up out of some underlying scalar. Au characterizes each such rep by that scalar — its ScalarOf<T> — and then applies the very same rules from above. So an Eigen::Vector3d, whose scalar is double, is governed by the floating point rule and never truncates; a vector of integers, on the other hand, follows the integral rules.

Other types

For a rep that is neither a built-in arithmetic type, nor a compound type with an arithmetic ScalarOf trait, Au has no basis to reason about truncation. Therefore, we fall back to the conservative assumption that the conversion can truncate, and forbid it by default. This is just a stopgap measure; we plan to support non-arithmetic reps more broadly in the future. See #52 to track any progress.

Truncation in casting

Besides mathematical operations, the other main operation in unit conversions is casting from one type to another. We assess truncation risk for these operations as well.

Arithmetic to arithmetic

Casting from one arithmetic type to another is governed by simple rules.

If the source and destination types are in the same category — that is, either both integral, or both floating point — then the cast never truncates. The reason for integral types is straightforward: clearly, the source can’t hold a non-integer value. As for floating point types, they are governed by the philosophy explained above.

Casting from a floating point type to an integral type does have truncation risk: we would truncate for any non-integer input. We can check this for individual values by discarding the fractional part, and checking whether this changes the value.

Casting from an integral type to a floating point type is an interesting case. As floating point values get larger, they grow farther apart. At some point, consecutive representable values can differ by multiple integers. If we cast an input integer in this range, it may seem that we should consider this to truncate. But recall the philosophy above: floating point types are all about relative position. For this reason, we consider casting from integral to floating point as a non-truncating operation.

Non-arithmetic types

Here, too, our support for non-arithmetic rep types is limited, and we take a conservative approach. Any cast involving a rep with non-arithmetic scalar type, either as source or destination, is considered to have truncation risk, and will not be allowed by default: users must pass ignore(TRUNCATION_RISK) as a second argument to override this. We hope to have better default behavior once we support non-arithmetic types more fully (#52).

Note that this policy tells you only whether the truncation rules object to a casting conversion. Passing those rules doesn’t mean the conversion will work: the reps involved must also support the cast in the first place.

As a concrete example, Eigen::Vector3d is a compound rep that does have an arithmetic scalar type, so the truncation rules have no objection to casting it to Eigen::Vector3f. Even so, the conversion doesn’t compile: Au can’t form a common rep for two Eigen types with different scalars, and so it never gets as far as the static_cast it would ordinarily perform. That’s no accident on Eigen’s part — Eigen deliberately rejects static_cast between different scalar types, directing users to its .cast<T>() member function instead. So this is the one case where you must reach for the cast<T> free function, which wraps .cast<T>() for use with Quantity.

Summary

Truncation happens when the result of an operation falls in-between representable values in the numeric type where we’re storing it. If any input values for a unit conversion can truncate — or, if we don’t know whether any can — then we forbid the conversion. Users can override this behavior by passing ignore(TRUNCATION_RISK) as a second argument. Users can also check whether individual values truncate using the will_conversion_truncate function.

Truncation risk depends strongly on the types involved. Integral types are vulnerable to truncation for non-integer scale factors. On the other hand, we treat floating point types as though they never “truncate”, because they’re already inexact. Finally, for compound reps such as complex numbers and Eigen vectors, we defer to the truncation rules of their underlying scalar type; only when that scalar can’t be determined do we fall back to a conservative assumption of truncation risk.


  1. “Arithmetic” types are C++’s built-in numeric types: int, double, uint64_t, and so on.