Skip to content

Magnitude

Magnitude is a family of monovalue types representing nonzero real numbers. These values can be multiplied, divided, and raised to (rational) powers, and this arithmetic always takes place at compile time. Values can also be converted to more standard numeric types, such as double and int, as long as the receiving type can represent the magnitude’s value faithfully.

The core motivation is to represent ratios of different units that have the same dimension. As a corollary, any unit can be scaled by a Magnitude to make a new unit of the same dimension.

Forming magnitudes

There are 3 valid ways for end users to form a Magnitude instance.

  1. ✔ Using the mag<N>() helper to form the canonical representation of the integer N.
  2. ✔ Writing Magnitude<MyConstant>{}, where MyConstant is a valid irrational magnitude base. (See the custom bases section below for more details.)
  3. ✔ Forming products, quotients, powers, and roots of other valid Magnitude instances.

The following is a valid, but dis-preferred way to form a Magnitude.

  • ⚠ Magnitude<>.
    • Explanation: This represents the number 1, but it’s less readable than writing mag<1>().

The following are not valid ways to form a Magnitude.

  • ❌ Magnitude<Pi, MyConstant>.
    • Explanation: Do not supply a manual sequence of template parameters. Magnitude has strict ordering requirements on its template parameters. The approved methods listed above are guaranteed to satisfy these requirements.
  • ❌ Magnitude<Prime<3>>.
    • Explanation: Do not supply integer bases manually. Integers are represented by their prime factorization, which is performed automatically. Instead, form integers, rationals, and their powers only by starting with valid Magnitude instances, and performing arithmetic operations as in option 3 above.

Below, we give more details on several concepts mentioned above.

mag<N>()

mag<N>() gives an instance of the unique, canonical Magnitude type that represents the positive integer N.

More detail on integral Magnitude representations

Integers are stored as their prime factorization. For example, 18 would be stored as the type Magnitude<Prime<2>, Pow<Prime<3>, 2>>, because 18 = 2 \cdot 3^2.

mag<N>() automatically performs the prime factorization of N, and constructs a well-formed Magnitude.

Custom bases

Magnitude can handle some irrational numbers. This even includes some transcendental numbers, such as \pi. Because Magnitude is closed under products and rational powers, this means that we also automatically support related values such as \pi^2, \frac{1}{\sqrt{2\pi}}, and so on.

What irrational numbers can Magnitude not handle?

A common example is any that are formed by addition. For example, (1 + \sqrt{2}) cannot be represented by Magnitude. Recall that Magnitude is designed to support products and rational powers, since these are the most important operations in quantity calculus.

It is tempting to want a better representation — one which supports full symbolic algebra. Perhaps such a representation could be designed. However, we haven’t seen any real world use cases for it. The current Magnitude implementation already handles the most critical use cases, such as handling \pi, which most units libraries have traditionally struggled to support.

Because of its importance for angular variables, \pi is supported natively in the library, via the irrational magnitude base, Pi. To define a magnitude instance for \pi, you can write:

constexpr auto PI = Magnitude<Pi>{};

If you need to represent an irrational number which can’t be formed via any product of powers of the existing Magnitude types — namely, integers and \pi — then you can define a new irrational magnitude base. This is a struct with the following member:

  • static constexpr long double value(): the best approximation of your constant’s value in the long double storage type.
Important information for defining your own constant

If you return a literal, you must add L on the end. Otherwise it will be interpreted as double, and will lose precision.

Here are the results of one example which was run on an arbitrary development machine.

No suffix L suffix
Literal 3.141592653589793238 3.141592653589793238L
Actual Value 3.141592653589793115 3.141592653589793238

The un-suffixed version has lost several digits of precision. (The precise amount will depend on the computer architecture being used.)

Each time you add a new irrational magnitude base, you must make sure that it’s independent: that is, that it can’t be formed as any product of rational powers of existing Magnitude types.

Extracting values

As a monovalue type, Magnitude can only hold one value. There are no computations we can perform at runtime; everything happens at compile time. What we can do is to extract that represented value, and store it in a more conventional numeric type, such as int or double.

To extract the value of a Magnitude instance m into a given numeric type T, call get_value<T>(m). This also works with Zero: get_value<T>(ZERO) returns T{0}.

Here are some important aspects of this utility.

  1. The computation takes place completely at compile time.
  2. The computation takes place in the widest type of the same kind. (That is, when T is floating point we use long double, and when T is integral we use std::intmax_t or std::uintmax_t according to the signedness of T.)
  3. If T cannot hold the value represented by m, we produce a compile time error.
Example: float and \pi^3

Suppose you are running on an architecture which has hardware support for float, but uses slow software emulation for double and long double. With Magnitude and get_value, you can get the best of both worlds:

  • The computation gets performed at compile time in long double, giving extra precision.
  • The result gets cast to float and stored as a program constant.

Thus, if you have a magnitude instance PI, then get_value<float>(pow<3>(PI)) will be much more accurate than storing \pi in a float, and cubing it — yet, there will be no loss in runtime performance.

Checking for representability

If you need to check whether your magnitude m can be represented in a type T, you can call representable_in<T>(m). This function is constexpr compatible.

Example: integer and non-integer values

Here are some example test cases which will pass.

EXPECT_THAT(representable_in<int>(mag<1>()), IsTrue());

// (1 / 2) is not an integer.
EXPECT_THAT(representable_in<int>(mag<1>() / mag<2>()), IsFalse());

EXPECT_THAT(representable_in<float>(mag<1>() / mag<2>()), IsTrue());
Example: range of the type

Here are some example test cases which will pass.

EXPECT_THAT(representable_in<uint32_t>(mag<4'000'000'000>()), IsTrue());

// 4 billion is larger than the max value representable in `int32_t`.
EXPECT_THAT(representable_in<int32_t>(mag<4'000'000'000>()), IsFalse());

Note that this function’s return value also depends on whether we can compute the value, not just whether it is representable. For example, representable_in<double>(sqrt(mag<2>())) is currently false, because we haven’t yet added support for computing rational base powers.

Compile-time arithmetic limitations

Some Magnitude operations need to perform arithmetic at compile time to compute their results. For example, to compare two magnitudes, we need to determine which one is larger. It is extremely hard, if not impossible, to do this for all magnitudes, which may involve arbitrary rational powers, and even transcendental numbers. However, others can easily be computed using only 64-bit integer operations.

We provide support for the subset of inputs where we can confidently produce exact answers, and guard other inputs behind compile-time errors. This enables many practical use cases, while conservatively avoiding producing an incorrect program.

Our concrete policy is to support only operations where we can produce exact answers using only 64-bit integer arithmetic. This does not mean that we only support exact integer magnitudes. These operations are binary, so the feasibility depends on the relationship between the two inputs.

Example

Suppose we had this variable:

constexpr auto PI = Magnitude<Pi>{};

Here are some examples of comparisons that would or would not be supported.

  • PI > mag<3>() would not be supported: it would result in a compiler error.
    • Reason: We cannot use the exact value of \pi in computations; we only have access to the nearest representable floating point number. Floating point arithmetic is inexact, so this does not meet our criteria.
  • PI > (mag<3>() * PI / mag<2>()) would be supported, and would evaluate to false.
    • Reason: Regardless of the exact value of \pi, we can see that \pi cancels out, so this comparison is equivalent to checking whether 1 > \frac{3}{2}, which we can convert to 2 > 3. This can be seen to be false using only integer arithmetic.

When an operation cannot be computed — either because the relationship involves irrational factors, or because the integers involved are too large — you will get a compile-time error with a message explaining the problem.

Note

Operations subject to this limitation will be marked with † in their documentation.

Operations

These are the operations which Magnitude supports. Because it is a monovalue type, the value can take the form of either a type or an instance. In what follows, we’ll use this convention:

  • Capital identifiers (M, M1, M2, …) refer to types.
  • Lowercase identifiers (m, m1, m2, …) refer to instances.

Equality comparison

Result: A bool indicating whether two Magnitude values represent the same number.

Syntax:

  • For types M1 and M2:
    • std::is_same<M1, M2>::value
  • For instances m1 and m2:
    • m1 == m2 (equality comparison)
    • m1 != m2 (inequality comparison)

Zero is never equal to any Magnitude:

  • ZERO == m is always false
  • ZERO != m is always true

Ordering comparison †

Result: A bool indicating the relative ordering of two Magnitude values.

Unlike equality comparison (which simply checks whether two types are the same), ordering comparison computes which magnitude is larger.

This feature is subject to compile-time arithmetic limitations.

Syntax:

  • For instances m1 and m2:
    • m1 < m2
    • m1 > m2
    • m1 <= m2
    • m1 >= m2

Zero can also be compared with any Magnitude:

  • ZERO < m is true when m is positive
  • ZERO > m is true when m is negative

Multiplication

Result: The product of two Magnitude values.

Syntax:

  • For types M1 and M2:
    • MagProduct<M1, M2>
  • For instances m1 and m2:
    • m1 * m2

Zero is an absorbing element:

  • ZERO * m equals ZERO
  • m * ZERO equals ZERO

Note

Older releases used MagProductT (with the T suffix) instead of MagProduct. Prefer MagProduct. MagProductT is deprecated, and will be removed in future releases.

Division

Result: The quotient of two Magnitude values.

Syntax:

  • For types M1 and M2:
    • MagQuotient<M1, M2>
  • For instances m1 and m2:
    • m1 / m2

Zero divided by any magnitude is Zero:

  • ZERO / m equals ZERO

Note

Older releases used MagQuotientT (with the T suffix) instead of MagQuotient. Prefer MagQuotient. MagQuotientT is deprecated, and will be removed in future releases.

Addition †

Result: The sum of two Magnitude values.

This operation also supports Zero as both an input and an output.

This feature is subject to compile-time arithmetic limitations.

Syntax:

  • For instances m1 and m2:
    • m1 + m2

Zero is the identity element:

  • ZERO + m equals m
  • m + ZERO equals m

If the sum is zero, the result type is Zero:

  • m + (-m) has type Zero

Subtraction †

Result: The difference of two Magnitude values.

This operation also supports Zero as both an input and an output.

This feature is subject to compile-time arithmetic limitations.

Syntax:

  • For instances m1 and m2:
    • m1 - m2

When the operands are equal, the result type is Zero:

  • m - m has type Zero

When the result is negative, the return type is a negative Magnitude:

  • mag<3>() - mag<5>() equals -mag<2>()

Zero interactions:

  • m - ZERO equals m
  • ZERO - m equals -m

Modulo †

Result: The remainder after dividing one Magnitude by another.

This operation uses truncated division semantics (matching C++ %): the result has the same sign as the dividend (first operand).

This operation also supports Zero as both an input and an output.

This feature is subject to compile-time arithmetic limitations.

Syntax:

  • For instances m1 and m2:
    • m1 % m2

When the dividend is exactly divisible by the divisor, the result type is Zero:

  • mag<6>() % mag<3>() has type Zero

When the dividend is Zero, the result is Zero:

  • ZERO % m has type Zero

Sign convention examples (result sign matches dividend):

  • mag<7>() % mag<3>() equals mag<1>()
  • (-mag<7>()) % mag<3>() equals -mag<1>()
  • mag<7>() % (-mag<3>()) equals mag<1>()
  • (-mag<7>()) % (-mag<3>()) equals -mag<1>()

Irrational common factors are handled correctly:

  • (mag<14>() * PI) % (mag<3>() * PI) equals mag<2>() * PI

When the divisor is Zero, the operation produces a compile-time error (since division by zero is undefined).

The result satisfies the division invariant: for any magnitudes a and b (with b nonzero), if q is the truncated quotient, then q * b + (a % b) equals a.

Negation

Result: The negative of a Magnitude.

Syntax:

  • For a type M:
    • No special support, but you can form the product with Magnitude<Negative>, which represents -1.
  • For an instance m:
    • -m

Powers

Result: A Magnitude raised to an integral power.

Syntax:

  • For a type M, and an integral power N:
    • MagPower<M, N>
  • For an instance m, and an integral power N:
    • pow<N>(m)

Note

Older releases used MagPowerT (with the T suffix) instead of MagPower. Prefer MagPower. MagPowerT is deprecated, and will be removed in future releases.

Roots

Result: An integral root of a Magnitude.

Syntax:

  • For a type M, and an integral root N:
    • MagPower<M, 1, N> (because the N^\text{th} root is equivalent to the \left(\frac{1}{N}\right)^\text{th} power)
  • For an instance m, and an integral root N:
    • root<N>(m)

Note

If m is negative, and N is even, then root<N>(m) produces a hard compiler error, because the result cannot be represented as a Magnitude.

Helpers for powers and roots

Magnitudes support all of the power helpers. So, for example, for a magnitude instance m, you can write sqrt(m) as a more readable alternative to root<2>(m).

Rounding helpers

These operations round a Magnitude to an integer Magnitude. They all support Zero as both an input and an output.

These features are generally subject to compile-time arithmetic limitations. We will include a warning directly in the subsection for each affected feature below.

Truncation †

Result: The integer part of a Magnitude, truncating toward zero.

This feature is subject to compile-time arithmetic limitations.

Syntax:

  • For an instance m:
    • mag_trunc(m)

Examples:

  • mag_trunc(mag<7>() / mag<3>()) equals mag<2>() (since \frac{7}{3} \approx 2.33)
  • mag_trunc(-mag<7>() / mag<3>()) equals -mag<2>() (truncates toward zero)
  • mag_trunc(mag<1>() / mag<3>()) has type Zero (since \frac{1}{3} \approx 0.33)
  • mag_trunc(ZERO) has type Zero

Floor †

Result: The largest integer Magnitude less than or equal to the input.

This feature is subject to compile-time arithmetic limitations.

Syntax:

  • For an instance m:
    • mag_floor(m)

Examples:

  • mag_floor(mag<7>() / mag<3>()) equals mag<2>() (since \lfloor 2.33 \rfloor = 2)
  • mag_floor(-mag<7>() / mag<3>()) equals -mag<3>() (since \lfloor -2.33 \rfloor = -3)
  • mag_floor(mag<1>() / mag<2>()) has type Zero (since \lfloor 0.5 \rfloor = 0)
  • mag_floor(-mag<1>() / mag<2>()) equals -mag<1>() (since \lfloor -0.5 \rfloor = -1)
  • mag_floor(ZERO) has type Zero

Ceiling †

Result: The smallest integer Magnitude greater than or equal to the input.

This feature is subject to compile-time arithmetic limitations.

Syntax:

  • For an instance m:
    • mag_ceil(m)

Examples:

  • mag_ceil(mag<7>() / mag<3>()) equals mag<3>() (since \lceil 2.33 \rceil = 3)
  • mag_ceil(-mag<7>() / mag<3>()) equals -mag<2>() (since \lceil -2.33 \rceil = -2)
  • mag_ceil(mag<1>() / mag<2>()) equals mag<1>() (since \lceil 0.5 \rceil = 1)
  • mag_ceil(-mag<1>() / mag<2>()) has type Zero (since \lceil -0.5 \rceil = 0)
  • mag_ceil(ZERO) has type Zero

Rounding †

Result: The nearest integer Magnitude to the input, with half-values rounding away from zero.

This feature is subject to compile-time arithmetic limitations.

Syntax:

  • For an instance m:
    • mag_round(m)

Examples:

  • mag_round(mag<7>() / mag<3>()) equals mag<2>() (since 2.33 is closer to 2)
  • mag_round(mag<8>() / mag<3>()) equals mag<3>() (since 2.67 is closer to 3)
  • mag_round(mag<5>() / mag<2>()) equals mag<3>() (since 2.5 rounds away from zero)
  • mag_round(-mag<5>() / mag<2>()) equals -mag<3>() (since -2.5 rounds away from zero)
  • mag_round(mag<1>() / mag<3>()) has type Zero (since 0.33 rounds to 0)
  • mag_round(ZERO) has type Zero

Traits

These traits provide information, at compile time, about the number represented by a Magnitude.

Is Integer?

Result: A bool indicating whether a Magnitude represents an integer (true if it does; false otherwise).

Syntax:

  • For a type M:
    • IsInteger<M>::value
  • For an instance m:
    • is_integer(m)

Is Rational?

Result: A bool indicating whether a Magnitude represents a rational number (true if it does; false otherwise).

Syntax:

  • For a type M:
    • IsRational<M>::value
  • For an instance m:
    • is_rational(m)

Is Positive?

Result: A bool indicating whether a Magnitude represents a positive number (true if it does; false otherwise).

Syntax:

  • For a type M:
    • IsPositive<M>::value
  • For an instance m:
    • is_positive(m)

Integer part

Result: The integer part of a Magnitude, which is another Magnitude.

For example, the “integer part” of \frac{\sqrt{18}}{5\pi} would be 3, because \sqrt{27} = 3\sqrt{2}, and 3 is the integer part of 3\sqrt{2}.

If the input magnitude is an integer, then this operation is the identity.

If the input magnitude is not an integer, then this operation produces the largest integer factor that can be extracted from the numerator (that is, the base powers with positive exponent).1

Syntax:

  • For a type M:
    • IntegerPart<M>
  • For an instance m:
    • integer_part(m)

Note

Older releases used IntegerPartT (with the T suffix) instead of IntegerPart. Prefer IntegerPart. IntegerPartT is deprecated, and will be removed in future releases.

Numerator (integer part)

Result: The numerator we would have if a Magnitude were written as a fraction. This result is another Magnitude.

For example, the “numerator” of \frac{3\sqrt{3}}{5\pi} would be 3\sqrt{3}.

Syntax:

  • For a type M:
    • Numerator<M>
  • For an instance m:
    • numerator(m)

Note

Older releases used NumeratorT (with the T suffix) instead of Numerator. Prefer Numerator. NumeratorT is deprecated, and will be removed in future releases.

Denominator (integer part)

Result: The denominator we would have if a Magnitude were written as a fraction. This result is another Magnitude.

For example, the “denominator” of \frac{3\sqrt{3}}{5\pi} would be 5\pi.

Syntax:

  • For a type M:
    • Denominator<M>
  • For an instance m:
    • denominator(m)

Note

Older releases used DenominatorT (with the T suffix) instead of Denominator. Prefer Denominator. DenominatorT is deprecated, and will be removed in future releases.

Absolute value

Result: The absolute value of a Magnitude, which is another Magnitude.

Syntax:

  • For a type M:
    • Abs<M>
  • For an instance m:
    • abs(m)

Sign

Result: A Magnitude: 1 if the input is positive, and -1 if the input is negative.

We expect that the relation m == sign(m) * abs(m) will hold for every Magnitude m.

Syntax:

  • For a type M:
    • Sign<M>
  • For an instance m:
    • sign(m)

  1. The concept integer_part() is conceptually ambiguous when applied to non-integers. So, too, for numerator() and denominator() applied to irrational numbers. These utilities serve two purposes. First, they provide a means for checking whether a given magnitude is a member of the unambiguous set — that is, we can check whether a magnitude is an integer by checking whether it’s equal to its “integer part”. Second, they enable us to automatically construct labels for magnitudes, by breaking them into the same kinds of pieces that a human reader would expect.