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.
-
Strong type definition.
- Required. Make a
struct
with the name you want, and inherit fromdecltype(u)
, whereu
is some unit expression which gives it the right Dimension and Magnitude. (We’ll explain unit expressions in the next section.)
- Required. Make a
-
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.
- A
-
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”, not “newtons 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 thanspeed.in(miles / hour)
.
-
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 aQuantity
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.
- Required. This gives you a
-
Quantity point maker.
- Just like the quantity maker, but conventionally with a
_pt
suffix to indicate that it makesQuantityPoint
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
, notQuantityPoint
.
- Just like the quantity maker, but conventionally with a
-
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:
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:
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.
-
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 asMeters{} / 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.
- Counterpoint: as seen below, this will reduce the ability to cancel out units. For
example,
- Example:
-
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 toMeters{}
, 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).
- Example:
-
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). ↩
-
Note that we don’t “need” to define this. We could write
(miles / hour)(65)
, and get exactly the same result asmiles_per_hour(65)
. However, some users may prefer the latter syntax. ↩