Skip to content

Defining new units

This page explains how to define new units that aren’t included in the library.

Tip

If it’s a common unit—one which should be in the library, but isn’t—go ahead and file an issue! We should be able to turn it around pretty quickly (either adding it to the library, or explaining why we won’t).

Definition features

Many libraries provide “convenience” macros for creating new units, but ours tries to avoid macros completely.1 Instead, you define new units by just writing regular C++ code.

There are several pieces you can add, each of which provides some particular feature. Here is a complete sample definition of a new Unit, with these features annotated and explained.

// Example custom unit definition below.
//
// Items labeled with `*` are _required_; everything else is optional.

// In .hh file:
struct Fathoms : decltype(Inches{} * mag<72>()) {           // *[1]
    static constexpr const char label[] = "ftm";            //  [2a]
};
constexpr auto fathom  = SingularNameFor<Fathoms>{};        //  [3]
constexpr auto fathoms = QuantityMaker<Fathoms>{};          // *[4]
constexpr auto fathoms_pt = QuantityPointMaker<Fathoms>{};  //  [5; less common]

namespace symbols {
constexpr auto ftm = SymbolFor<Fathoms>{};                  //  [6]
}

// In .cc file:
constexpr const char Fathoms::label[];                      //  [2b]
// Example custom unit definition below.
//
// Items labeled with `*` are _required_; everything else is optional.

// In .hh file:
struct Fathoms : decltype(Inches{} * mag<72>()) {           // *[1]
    static constexpr inline const char label[] = "ftm";     //  [2]
};
constexpr auto fathom  = SingularNameFor<Fathoms>{};        //  [3]
constexpr auto fathoms = QuantityMaker<Fathoms>{};          // *[4]
constexpr auto fathoms_pt = QuantityPointMaker<Fathoms>{};  //  [5; less common]

namespace symbols {
constexpr auto ftm = SymbolFor<Fathoms>{};                  //  [6]
}

Note

If you’ve seen the unit definitions included in our library, you may notice they look a little different from the above. That’s because the library has different goals and constraints than end user projects have.

For example, the library needs to be both C++14-compatible and header-only. This forces us to define our labels in a more complicated way. By contrast, your project is unlikely to have both these constraints.

Prefer the simpler approach outlined in this page, instead of treating our library’s source code definitions as examples to follow.

Here are the features.

  1. Strong type definition.

    • Required. Make a struct with the name you want, and inherit from decltype(u), where u is some unit expression which gives it the right Dimension and Magnitude. (We’ll explain unit expressions in the next section.)
  2. Label.

    • A sizeof()-compatible label which is useful for printing the Unit.
    • Note that if your project needs C++14 compatibility, then besides the label itself ([2a]), you’ll need to provide a definition ([2b]) in the .cc file. By contrast, if you use C++17 or later, you can just use an inline variable, and you won’t need a .cc file.
    • If omitted: Everything will still work; your Quantity will just be labeled as [UNLABELED UNIT] in printing contexts.
  3. Singular name.

    • An object whose name is the singular name for your unit. Useful in certain contexts: for example, the traditional unit for torque is “newton meters”, notnewtons meters”.
    • If omitted: you’ll sacrifice some readability flow: the grammar becomes strange. You’ll end up with constructs like speed.in(miles / hours), rather than speed.in(miles / hour).
  4. Quantity maker.

    • Required. This gives you a snake_case version of your unit which acts like a function. If you call this “function” and pass it any numeric type, it creates a Quantity of your unit, whose Rep is that type. Of course, a quantity maker is much more than a function: it composes nicely with prefixes, and with other quantity makers.
  5. Quantity point maker.

    • Just like the quantity maker, but conventionally with a _pt suffix to indicate that it makes QuantityPoint instead. You can call this like a function on arbitrary numeric types. You can also compose it with prefixes, or scale it with Magnitudes.
    • If omitted: this is usually fine to omit: most Units are only used with Quantity, not QuantityPoint.
  6. Unit symbol.

    • This lets you create quantities of this unit by simply multiplying or dividing raw numbers. You can also change the units of existing quantities in the same way. See the docs for unit symbols.
    • If omitted: Users will either need to create their own symbols on the fly, or else spell out the full name of the unit.

Note

Not shown here: adding an origin member. We skipped this because it is very rare. It only has any effect at all for Units you plan to use with QuantityPoint, which is not the usual case. Even among those units, only a small subset have a non-default origin. The main examples are Celsius and Fahrenheit, and the library will provide those out of the box.

Unit expressions

Above, we said to inherit your unit’s strong type from the decltype of a unit expression. Recall the line from above:

struct Fathoms : decltype(Inches{} * mag<72>()) {
//        Unit Expression ^^^^^^^^^^^^^^^^^^^^

Some users may be surprised that we recommend using decltype and instances, instead of just naming the type directly. The reason we do is that in C++ code generally, instances are easier to compose than types. This is especially true for units, which support a variety of operations: multiplying and dividing by other units, scaling by magnitudes, and even raising to rational powers.

If we used types directly, users would need to learn obscure new traits, like UnitProductT for unit-unit products, and ScaledUnit for unit-magnitude products. With instances, we can simply write * as we would for any other kind of instances — and this * covers both use cases! Wrapping the result in decltype(...) is a small price to pay for this familiarity and flexibility.

Here are some example unit expressions we might reach for to define various common units:

  • Newtons: Kilo<Grams>{} * Meters{} / squared(Seconds{})
  • Miles: Feet{} * mag<5280>()
  • Degrees: Radians{} * PI / mag<180>()

Aliases vs. strong types: best practices

A shorter method of defining units is as aliases for a compound unit. For example:

using MilesPerHour = decltype(Miles{} / Hours{});
constexpr auto miles_per_hour = miles / hour;

We can use the alias, MilesPerHour, anywhere we’d use a unit type. And we can call the QuantityMaker, miles_per_hour, just as we would call miles.2 We even get an automatically generated unit label: mi / h.

Despite this convenience, aliases aren’t always the best choice. Here’s the best practices guidance to follow.

  1. Use strong types for named units.

    • Example: Newtons; Fathoms
    • Rationale: Strong types show up in compiler errors, making them easier to read.
      • Counterpoint: as seen below, this will reduce the ability to cancel out units. For example, Meters{} * Hertz{} will not be the same as Meters{} / Seconds{}; instead, it will be a different-but-equivalent Unit. Given the way we handle quantity-equivalent Units, this will usually not be a problem, and we believe the value of seeing shorter, more familiar names in the compiler errors outweighs this cost.
  2. Use aliases for compound units with no special name.

    • Example: NewtonMeters; MilesPerHour. Both of these are better implemented as aliases rather than strong types.
    • Rationale: Keeping these as aliases increases support for cancellation: it enables the library to notice that MetersPerSecond{} * Seconds{} is identical to Meters{}, not merely quantity-equivalent. This doesn’t usually matter, but it can reduce exposure to compiler errors in the (rare) situations where exact-type-equality matters (e.g., initializer lists).

  1. Macros have long been considered contrary to C++ best practices. If we’re going to use one, especially in user-facing code, it needs to meet a very high bar. Unit definition macros don’t meet this bar. They mostly exist to save typing. But code is read far more often than written, and macros actually make the definitions harder to read and understand (because they use positional arguments, so the meaning of the parameters is unclear at the callsite). 

  2. Note that we don’t “need” to define this. We could write (miles / hour)(65), and get exactly the same result as miles_per_hour(65). However, some users may prefer the latter syntax.