|
Unit Conversion and Dimensional Analysis Library 3.6.1
A compile-time, header-only C++23 dimensional-analysis library
|
The safety units provides lives in the type system. Types do not exist at run time, so neither does the safety's cost: an optimized build produces the same machine code as the equivalent hand-written arithmetic on plain numbers.
Dimensional correctness has no run-time cost. Every check happens during compilation; every conversion factor is known to the compiler; every quantity is a trivial value the size of the number it holds. This page explains why, with examples you can compile and inspect.
A unit is defined as a std::ratio relative to its dimension's base unit. feet is std::ratio<381, 1250> of a meter; inches is std::ratio<1, 12> of a foot; and so on. Because those ratios are types, the conversion factor between any two units of the same dimension is computed by the type system as a compile-time constant. Converting a quantity multiplies its stored value by that constant.
The 381/1250 (and its reciprocal for the other direction) is resolved before code generation. At run time this is a single floating-point multiply — the same instruction you would have written by hand had you remembered the factor and typed it correctly.
Units are defined in chains. In units/time.h:
Nothing in that chain defines "years to weeks" directly. If the chain were followed at run time, a years → weeks conversion would walk years → days → weeks, and a years → seconds conversion would walk five links. It is not followed at run time. std::ratio multiplication composes the whole chain into a single reduced ratio at compile time:
Both static_asserts pass, which is proof that the results are available as compile-time constants. The five-link years → seconds conversion emits, at most, one multiply — and when the source value is itself a constant, as above, it emits nothing at all: the answer is baked in.
When the source and destination are the same type, there is nothing to convert. The library does not insert a multiply-by-one; the assignment is an ordinary copy of the underlying value.
The same holds when two type spellings denote the identical type. meters, meters<>, and meters<double> name one type; assigning between them is a copy, never a conversion. (See the namespace map and CTAD and ADL for why those spellings coincide.)
A quantity holds exactly one number. It adds no vtable, no tag, no bookkeeping field — the unit is encoded in the type, which occupies no storage. The standard traits confirm this:
Trivial copyability is a load-bearing property, deliberately preserved by the named-unit types: it means a quantity can be memcpy'd, passed in a register, and placed in constexpr and trivially-relocatable contexts exactly as its underlying type can. meters<int> is trivially copyable and the size of an int; meters<float> the size of a float; and so on.
Because conversions and the algebraic operations are constexpr, an entire calculation can be evaluated by the compiler and checked with a static_assert — which simultaneously demonstrates that no run-time work remains, since a value the compiler can assert on is a value it has already computed:
The division of a length by a time produces the correct velocity type by type-system arithmetic, and the numeric result is a compile-time constant. There is no run-time division, no unit lookup, and no dispatch.
For an optimized build, the code generated for typed-quantity arithmetic matches the code generated for the corresponding plain-double arithmetic. The disassembly below is -O2 unless noted; GCC 15 and Clang 21 agree.
A runtime expression. Compute a distance from a speed in mph and a time in seconds — the raw version hard-codes the mph → m/s factor, the units version carries it in the types:
Both are three floating-point instructions — a multiply, a divide, a multiply — differing only in operand order (GCC 15):
A same-unit conversion is nothing. Passing a meters where a meters is wanted is not a cheap conversion — the function is a single ret:
A compile-time conversion is done by the compiler. A conversion of known values folds to a single constant load; the arithmetic never runs:
A hot loop vectorizes identically. Summing an array of kilometers as meters produces the same instruction stream — including the AVX vectorization at -O3 -march=x86-64-v3 — as the equivalent raw-double loop.
In each case the units version carries no wrapper, no extra load, and no branch: the type is gone, and only the arithmetic remains.
Most of the API is usable in a constant expression: construction, conversion, comparison, the arithmetic operators, and the algebraic math functions sqrt and pow, which are implemented with the library's own compile-time rational routines.
Two areas are the exception, and for the same underlying reason — they call standard-library functions that are not themselves constexpr:
Concretely:
The first line compiles; the second does not, because cos cannot run at compile time. This is a property of the standard functions being wrapped, not of units. At run time both are ordinary, optimizable calls — the caveat is strictly about compile-time evaluation.
Caveat: "not `constexpr`" means "not usable in a constant expression such as a `static_assert` or a template argument." It does not mean slow. The trigonometric wrappers compile to a single call to the standard-library function, with the angle already converted to radians by a compile-time ratio. Their run-time cost is exactly that of calling std::sin yourself.
See also: why use a units library · the namespace map · CTAD and ADL · scales · the cheat sheet