Skip to content

Parameter packs

Products of base powers are the foundation for the Au library. We use them for:

  • The Dimension of a Unit.
  • The Magnitude of a Unit.
  • Making compound Units (products of powers of units, e.g., \text{m} \cdot \text{s}^{-2}).

We represent them as variadic parameter packs. Each pack element represents a “base power”: this is some “base”, raised to some rational exponent. For a base power BP, BaseT<BP> retrieves its base, and ExpT<BP> retrieves its exponent (as a std::ratio).

Note

This approach, with products of base powers, is known as the vector space representation for Dimensions, Magnitudes, and so on. The //au:packs target, which this page describes, is our tool for implementing these vector spaces robustly.

Representing powers

These packs show up in compiler errors, and we want those errors to be as friendly as possible. Clutter is our enemy! Thus, we canonicalize each base power to its simplest form. Consider an arbitrary base type, B; here is how it shows up in the pack:

This power of B …shows up in a pack as:
B ^ 0 (omitted)
B ^ 1 B
B ^ N, with N any other integer Pow<B, N>
B^{ N / D }, with D > 1 RatioPow<B, N, D>

Canonicalizing in this way keeps our compiler errors more concise and readable.

Strict total ordering

The above canonicalization tells us what items to store. We also need to be careful about which order to store them in. We are modeling multiplication, and in our applications, (A \times B) is always the same as (B \times A). However, Pack<A, B> is not the same type as Pack<B, A>! Thus, we are going to need a way to define whether A or B should come first inside of a Pack.

What we need is a strict total ordering, which applies to all types which might represent a Base in a given kind of Pack. This is a critical foundational concept for the library, so we use explicit traits for each kind of pack. There are two main elements to this API:

  • InOrderFor<Pack, A, B> is for generic algorithms. It’s how we check whether A and B are in the right order for Pack.

  • LexicographicTotalOrdering<A, B, Orderings...> is for implementing InOrderFor for a given Pack. It’s how we define whether A and B are in order for Pack.

The point in using LexicographicTotalOrdering is that it guards against the most common failure mode in our application: namely, two distinct types which compare as equivalent. LexicographicTotalOrdering tries A and B against every comparator in Orderings..., in sequence. If any comparator knows how to order A and B, we use it. If we run out of comparators, but A is not the same as B, then we produce a hard error. The fix is to add a new comparator to “break the tie”.

Example: defining the ordering for a Pack

Suppose we have a particular pack, Pack, and our bases are std::ratio instances. We need to define some canonical ordering. Let’s say that we want to order first by denominator—integers first, then halves, thirds, etc—and then by numerator. We can define traits for those orderings, and then combine those traits using LexicographicTotalOrdering to implement InOrderFor<Pack, ...>. Specifically:

template <typename A, typename B>
struct OrderByDenom : stdx::bool_constant<(A::den < B::den)> {};

template <typename A, typename B>
struct OrderByNum : stdx::bool_constant<(A::num < B::num)> {};

template <typename A, typename B>
struct InOrderFor<Pack, A, B> :
    LexicographicTotalOrdering<A, B, OrderByDenom, OrderByNum> {};

With this definition, something like Pack<std::ratio<-1>, std::ratio<8>, std::ratio<1, 2>> would be in-order.

Validation

We validate packs using type traits. IsValidPack<Pack, T> is the “overall” validator. It verifies that T is an instance of Pack<...>, and that its parameters satisfy the necessary conditions. Specifically, those conditions are:

  • AreBasesInOrder<Pack, T>: assuming T is Pack<BPs...>, verifies that all consecutive elements in BaseT<BPs>... are all properly ordered (according to InOrderFor<Pack, ...>, naturally).

  • AreAllPowersNonzero<Pack, T>: assuming T is Pack<BPs...>, verifies that Exp<BPs>::num is nonzero for every element in BPs.

Algebra on Packs

The whole reason we built //au:packs was to support exact symbolic algebra for two operations: products, and rational powers. This section explains how we do that. Our strategy is:

  • The //au:packs target provides generic versions of these operations that are pre-built, but cumbersome.

    • (What makes them cumbersome? They need an extra parameter to specify which Pack they operate on. This is much like InOrderFor, which defines the ordering for a specific type of Pack.)
  • Client targets provide aliases which “hide” the extra parameter (because they know what value it should take!).

Let’s take the “pack product” operation as an example, using Dimension as our Pack:

// The `//au:packs` library provides this:
template <template <class...> typename Pack, typename... Ts>
using PackProductT = /* (implementation; irrelevant here) */;

// A _particular_ Pack (say, `Dimension`) would expose it to their users like this:
template <typename... Dims>
using DimProductT = PackProductT<Dimension, Dims...>;

// End users would use the _latter_, e.g.:
using Length = DimProductT<Speed, Time>;

Supported algebraic operations

Here are the operations we support:

  • PackProductT<Pack, Ps...>: the product of arbitrarily many (0 or more) Pack<...> instances, Ps....
  • PackQuotientT<Pack, P1, P2>: the quotient P1 / P2.
  • PackPowerT<Pack, P, N, D=1>: raise the Pack P to the rational power N / D.
  • PackInverseT<Pack, P>: the Pack that gives the null pack when multiplied with the Pack P.