nholthaus/units

a compile-time, header-only, dimensional analysis and unit conversion library built on c++14 with no dependencies.

C++

1,059

679 commits

updated Sep 18, 2026

See the code

README

units

A compile-time, header-only, dimensional-analysis and unit-conversion library for C++23, with no dependencies.

Windows build GCC build Clang build license copyright standard

units represents physical quantities as types. A quantity is a value with a unit — meters, feet, seconds — that behaves like the number it wraps. Conversions between compatible units are implicit and resolved at compile time; expressions that are dimensionally inconsistent do not compile.

Quantities are written with unit literals (5.0_m) or by multiplying a value by a unit constant (60.0 * km):

#include <units/length.h>
#include <iostream>

int main()
{
    using namespace units;
    using namespace units::literals;

    meters a = 5.0_m;        // unit literal
    meters b = 60.0 * km;    // value times a unit constant (== 60000 m)
    feet   c = a;            // implicit, lossless conversion

    std::cout << a << ", " << b << ", " << c << '\n';   // prints: 5 m, 60000 m, 16.4042 ft
}

units favors syntax that reads as ordinary code: quantities are written and combined the way you would write them by hand, so the common cases are apparent from the code without consulting the reference.

Every snippet in this README and in the documentation is compiled and run as part of the test suite — see examples/.

Design

The library is organized around syntax that reads as ordinary arithmetic. A quantity is constructed with a unit literal (5.0_m) or a unit constant (60.0 * km), combined with the usual operators (+, *, /, comparisons), converted by assignment, and printed with <<. The common operations are intended to work as written; the deeper machinery (class-based named types, CTAD, ADL) exists so that this surface stays small and the code stays legible.

Contents


Features

  • Syntax. Quantities are written as meters, 60_mi / 1_hr, sqrt(area); operations are performed on the quantity types.
  • Batteries included. Over 280 units across 48 dimensions ship ready to use — SI, imperial and US-customary, ancient, and esoteric alike (3.0_cwt, 10.0_fur, 1.0_rem), each a named type with a literal and an exact, canonically-sourced conversion ratio. There is no unit "system" to select or instantiate: every unit is first class and used directly, and units from different systems combine in one expression (1.0_m + 3.0_ft + 1.0_fur). Nothing to configure, no unit to define before use.
  • Run-time cost. Conversions are constexpr ratios; a conversion between equivalent representations compiles to no machine code. A quantity is a trivially-copyable value the size of its underlying type.
  • Dimensional checking. Adding a length to a time, or assigning an area to a length, is a compile error. The dimensional analysis is performed by the type system.
  • Decibel and logarithmic scales. A unit's scale is part of its type. Alongside the default linear scale, decibel_scale provides dBW, dBm, and the dimensionless dB, with scale-correct arithmetic — adding decibels multiplies the underlying linear quantities. A decibel-scale unit requires a floating-point underlying type.
  • Diagnostics. A dimensional error names the unit type (meters<double>) rather than the conversion_factor<...> template. See Type errors.
  • Linear algebra with Eigen. Store dimensioned quantities in Eigen vectors and matrices with the dimensions checked at compile time — Eigen::Matrix<meters<double>, 3, 1>. Optional and dependency-free (activates only if Eigen is present). See Linear algebra with Eigen.
  • Self-describing serialization. serialize(q) writes a quantity to a compact binary stream that carries its dimension as well as its value; a reader recovers it with no prior agreement on the type, and the format extends to any base dimension — including your own make_dimension<> — with no central table and no reflection. See Serialization.
  • Trivial integration. Header-only, no dependencies, one #include. Drop in the headers, or consume the CMake package. See Integration.

Requirements

units requires a C++23 compiler. It is continuously tested on:

CompilerVersionPlatform
GCC (g++)13Ubuntu (latest)
Clang (clang++)19Ubuntu (latest)
MSVC (Visual Studio)2022Windows Server 2022
MSVC (Visual Studio)2026Windows Server 2025

Older toolchains are not supported by the 3.x line. The last release for the C++14 era is the 2.x series (see Migrating from 2.x).


Getting started

This section covers what most code needs. The full manual has the rest.

Include a header, and bring in the literal operators. Include the umbrella header <units.h> for every dimension, or one per-dimension header (<units/length.h>, <units/time.h>, …) for just the dimensions you use:

#include <units.h>            // everything; or <units/length.h>, <units/velocity.h>, ... for a subset
using namespace units;
using namespace units::literals;   // the _m, _s, _kg, ... literals

Note — if compiles are slow, include less. <units.h> pulls in all 48 dimensions. The library is heavily templated, so a translation unit's compile time scales with how much it instantiates; including only the per-dimension headers you use keeps it down. As a best practice — not a requirement — include the dimension a computed result lands in (dividing a length by a time yields a velocity, so <units/velocity.h> lets you name and print that result as meters_per_second); a translation unit that omits it still computes the correct value and dimension, just under the plain unit<...> type. Run-time behavior and code size are unaffected either way.

Make a quantity. Four equivalent forms:

meters a(5.0);          // construction (CTAD deduces meters<double>)
meters b = 5.0_m;       // a unit literal
meters c = 5.0 * m;     // a value times a unit constant (units::m)
meters d{5.0};          // braced construction

Note — unit literals are floating-point. Both 5.0_m and 5_m are meters<double>, so 1_m / 2_m is 0.5, not 0 — a literal never silently does integer arithmetic. An integer representation is opt-in through the type (meters<int> n{5}, or CTAD from an integer initializer meters a(5)); a compile-time value that is exact narrows into it (meters<int> n = 5_m;), while a fractional one (5.5_m) is a compile error rather than a silent truncation.

Spelling the type: meters, meters<>, meters<T>. Three ways to name the type:

  • meters<T> — an explicit representation (meters<double>, meters<float>, meters<int>). Valid as a variable, function parameter, return type, or member.
  • meters<> — the default representation; identical to meters<double>. Valid in the same positions.
  • meters — the bare name deduces the representation from the initializer (CTAD): meters a(5.0) is meters<double>, meters a(5) is meters<int>. Valid only where an initializer is present to deduce from — a local variable. A function parameter, a return type, and a class member have no initializer, so they require meters<> or meters<T>.
meters        local(5.0);      // bare name, deduced meters<double>
meters<>      m;               // default representation
meters<float> as_float(5.0f);  // explicit representation
// void f(meters q);           // ill-formed: a parameter has no initializer to deduce from
// meters make();              // ill-formed: a return type has no initializer to deduce from

auto vs. an explicit type. Use auto on the left when the right-hand side already states the unit:

auto d = 5.0_m;                  // meters
auto speed = 60.0_mi / 1.0_hr;   // a velocity

Write the type explicitly on the left when the compiler should confirm the dimensional analysis: naming the result type makes a mismatch a compile error rather than an accepted auto deduction.

square_meters     area  = 15.0_m * 5.0_m;    // the result is an area
meters_per_second speed = 100.0_m / 8.0_s;   // the result is a velocity
// meters bad = 15.0_m * 5.0_m;              // ill-formed: the result is an area, not a length

Convert by assigning between compatible units (implicit, and only when lossless):

meters m = 100.0_ft;    // feet -> meters
feet   f = m;           // meters -> feet

Do arithmetic — the result carries the correct dimension; name it and the compiler checks it:

square_meters     area  = 15.0_m * 5.0_m;    // m * m -> area
meters_per_second speed = 60.0_mi / 1.0_hr;   // -> velocity
meters            side  = sqrt(area / 3.0);   // <cmath> functions are unit-aware (found by ADL)

Get a plain number back out at the boundary with non-units code (there is no implicit quantity → double, except for dimensionless quantities):

double v = speed.value();     // the value in the quantity's units
double t = speed.to<double>();// cast to a chosen representation
std::cout << speed;           // or print it directly: "26.8224 mps"

That is enough for most use cases. How meters a(5.0) deduces its type and why sqrt needs no units:: prefix are covered in CTAD and ADL; the full walkthrough is in Getting started.

Temperatures and other affine units

A few units are affine: they carry a datum offset, not just a scale. Degrees Celsius and Fahrenheit are the common examples — 0 °C is 273.15 K, not 0 K. An affine quantity is an absolute point on a scale, and a difference of two points is a delta (an amount of temperature, with no datum). The library keeps this distinction quiet — you write ordinary arithmetic and it does the physically correct thing — but the rules are worth knowing:

using namespace units::temperature;

// point − point → a delta (the datum offsets cancel):
auto d = celsius<double>(100.0) - fahrenheit<double>(32.0);   // 100 K  (a temperature difference)
kelvin<double>(celsius<double>(0.0) - kelvin<double>(0.0));   // 273.15 K

// point ± delta → move the point (compound assignment reads the rhs as a relative amount):
celsius<double> t(20.0);
t += celsius<double>(5.0);   // warm by 5 degrees → 25 °C   (still an absolute temperature)
t -= celsius<double>(10.0);  // cool by 10 degrees → 15 °C

// point + point is disabled for OFFSET scales — summing two degrees-Celsius values adds their datums,
// which is arithmetically corrupt (20 °C + 5 °C is not 25 °C in any absolute sense):
// auto bad = celsius<double>(20.0) + celsius<double>(5.0);   // does not compile (by design)
// Zero-offset scales (kelvin, rankine) have no datum to corrupt, so they add like any linear unit.

Comparisons (==, <, …) and conversions between affine units are exact and always available. Non-affine units (lengths, masses, everything without an offset) are unaffected — they add, subtract, and combine with no special cases.

Opt-in absolute<> / delta<> wrappers

The point/delta distinction above is enforced quietly on plain units. When you want the compiler to enforce it in the type — so a function that takes a temperature difference cannot be handed an absolute temperature, or an epoch cannot be added to an epoch — reach for the two opt-in wrappers.

They live in their own header, so they are strictly opt-in: nothing about them exists until you ask for it.

#include <units/kind.h>   // the wrappers exist only where you include this

Without that include (even with <units.h>) there is no absolute, no delta, no kind — so your own names by those spellings are never disturbed, and there is no cost. Once included, the two wrappers are:

  • absolute<U> — a point on a scale. It carries the unit's datum: absolute<celsius<double>>(0.0) is 273.15 K, and .to<kelvin<double>>() applies the offset.
  • delta<U> — an amount (offset-free). delta<celsius<double>>(10.0).to<fahrenheit<double>>() is an 18 °F delta (scale only), never an absolute 50 °F.

They wrap any unit (for a non-affine unit the datum is zero, so the two coincide numerically), are trivially copyable, and are exactly the size of the wrapped unit — zero overhead. The type algebra, enforced at compile time:

using namespace units;
using namespace units::temperature;

// point − point → delta (the datum cancels), kept in the LEFT operand's unit:
auto span = absolute<celsius<double>>(100.0) - absolute<fahrenheit<double>>(32.0);  // delta of 100 °C-degrees
static_assert(traits::is_delta_v<decltype(span)>);

// point ± delta → point (move the point), delta ± delta → delta, delta */ scalar → delta:
auto warmer = absolute<celsius<double>>(20.0) + delta<celsius<double>>(5.0);        // 25 °C (a point)
delta<celsius<double>> d(10.0);
d += delta<fahrenheit<double>>(18.0);   // +10 °C-degrees (mixed unit converts by degree size)
d *= 2.0;                               // 40 °C-degrees

// point + point is ill-formed — the sum of two points has no meaning:
// auto bad = absolute<celsius<double>>(20.0) + absolute<celsius<double>>(5.0);     // does not compile

Result unit — the LHS-unit tie-break. A wrapper arithmetic operator keeps the left operand's unit, so .value() reads in the unit you wrote: absolute<celsius> − absolute<fahrenheit> is a difference in celsius-degrees (reads 100), not a value in some common sub-unit. The only adjustment is to the underlying type: when the left operand's underlying is integral and cannot hold the right operand losslessly (e.g. absolute<kilometers<int>> − absolute<meters<int>>), the underlying promotes to floating point while the unit stays kilometers (the delta reads 0.5). Comparisons reconcile to the common (finer) unit so a mixed integer comparison never narrows.

Traits and concepts let a template constrain on the role: traits::is_absolute_v<T>, traits::is_delta_v<T>, and the concepts AbsoluteType / DeltaType. units::abs/min/max/clamp work on a delta; min/max/ clamp work on an absolute. Streaming and units::to_string forward to the wrapped quantity, prefixing a delta marker so a difference is visually distinct from a point.

One conversion verb, to<Target>(), crosses between types: a plain-unit target unwraps (a point applies its datum, a delta is scale-only), a wrapper target stays wrapped — c.to<kelvin<double>>() is a plain 273.15 K, c.to<absolute<kelvin<double>>>() stays a point. There is no .quantity() accessor; to<PlainUnit>() is the way out.

String-tagged kind<Tag, U> — same unit, different kind. When two quantities share a unit and a dimension yet are semantically different — a radial vs. a straight-line distance, a torque vs. an energy — tag each with a compile-time string so the types don't mix:

#include <units/kind.h>
using namespace units;
using namespace units::length;

kind<"radial",   meters<double>> r(5.0);
kind<"straight", meters<double>> s(3.0);
auto ok  = r + kind<"radial", meters<double>>(1.0);   // same kind → radial 6 m
// auto no = r + s;                                    // ill-formed: "cannot add two DIFFERENT kinds"

A plain unit is constructible into a kind by deliberate assignment but does not mix with one in arithmetic; a tag mismatch produces a readable, tag-naming diagnostic. absolute/delta live in an inline namespace affine, so units::absolute and units::affine::absolute name the same type — qualify only to disambiguate.

See the how-to guide absolute & delta wrappers for the full type algebra, the datum rules, kind<>, and worked examples, and affine temperature for the why.

Serialization of the wrappers is not yet supported (a follow-up); unwrap with to<PlainUnit>() and serialize that.


Type errors

A dimensional mistake that a bare double would accept is rejected at compile time, and the diagnostic names the unit type. The messages below are captured verbatim from GCC 13.

Adding incompatible dimensions:

readable_add_incompatible.cpp:9:18: error: no match for ‘operator+’ (operand types are ‘units::length::meters<double>’ and ‘units::time::seconds<double>’)
    9 | auto bad = 1.0_m + 1.0_s; // ill-formed: cannot add length and time
      |            ~~~~~ ^ ~~~~~
      |            |       |
      |            |       units::time::seconds<double>
      |            units::length::meters<double>

Assigning a product to the wrong dimension — m * m is an area, not a length. GCC reports the result through an internal alias with the named type beside it in {aka …}:

readable_wrong_result_type.cpp:10:41: error: conversion from ‘units::detail::rewrap_to_named_t<units::unit<units::area::square_meters_, double, units::linear_scale> >’ {aka ‘units::area::square_meters<double>’} to non-scalar type ‘units::length::meters<double>’ requested
   10 | units::length::meters<double> a = 1.0_m * 1.0_m; // ill-formed: m*m is an area, not a length
      |                                   ~~~~~~^~~~~~~

The full set of rejected operations, with the diagnostic each produces on GCC, Clang, and MSVC, is in Type safety. The diagnostics there are captured from the compilers by the test harness.


Run-time cost

A quantity is a trivially-copyable value the size of its underlying type; conversion ratios are constexpr. The type abstraction compiles away: the generated code matches hand-written double. The following disassembly is at -O2 (-O3 -march=x86-64-v3 for the loop); GCC 15 and Clang 21 agree.

A runtime expression compiles to the same instructions. Computing 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 — yields three floating-point instructions either way (a multiply, a divide, a multiply), differing only in operand order:

double         distance_raw  (double mph, double sec)                        { return (mph * 1609.344 / 3600.0) * sec; }
meters<double> distance_units(miles_per_hour<double> v, seconds<double> t)   { return v * t; }
distance_raw:                        distance_units:
    mulsd   .LC0(%rip), %xmm0            mulsd   %xmm1, %xmm0
    divsd   .LC1(%rip), %xmm0            mulsd   .LC2(%rip), %xmm0
    mulsd   %xmm1, %xmm0                 divsd   .LC3(%rip), %xmm0
    ret                                  ret

A conversion between equivalent representations is free. Passing a meters where a meters is wanted is not a cheap conversion — it is no conversion:

double roundtrip(meters<double> m) { meters<double> copy = m; return copy.value(); }
roundtrip:
    ret                              ; the whole function

A compile-time conversion is done by the compiler. A conversion of known values folds to a single constant load — the arithmetic never runs:

double speed_limit_mps() { return meters_per_second<double>(65.0_mph).value(); }
speed_limit_mps:
    movsd   .LC0(%rip), %xmm0        ; xmm0 = 29.0576  (65 mph, converted at compile time)
    ret

A hot loop vectorizes the same. Summing an array of kilometers as meters produces the identical instruction stream — including the AVX vectorization — as the raw-double loop; and

static_assert(1.0_km + 1.0_m == 1001.0_m);   // evaluated at compile time

holds with no run-time work. See Efficiency for the full comparison.


Linear algebra with Eigen

units composes with Eigen so you can carry dimensions through vectors and matrices — a rotated position, a moment computed from a lever arm and a force, a velocity integrated over a step — with the dimensional analysis still done by the type system, and with no dependency added to either library. The support activates automatically when <Eigen/Core> is on your include path and is a no-op when it is not (guarded by __has_include, exactly like the optional JSON support); there is no build flag to set.

#include <Eigen/Core>
#include <units.h>

using namespace units;
using namespace units::literals;

Eigen::Matrix<meters<double>, 3, 1> position;
position << 1.0_m, 2.0_m, 2.0_m;

auto           doubled = position * 2.0;       // scale — still a vector of meters
auto           sum     = position.sum();       // 5 m
meters<double> range   = unit_norm(position);  // 3 m — the norm returns to the original dimension

A vector holds one scalar type, so same-dimension operations — construction, +/-, scaling, sum(), block and Map views, cast() — work directly on Eigen expressions. The operations whose result changes dimension (a dot product of lengths is an area; a cross product carries the product dimension) are provided as helpers that compute the dimensionally-correct type:

#include <units/area.h>

Eigen::Matrix<meters<double>,  3, 1> arm;
Eigen::Matrix<newtons<double>, 3, 1> force;
// ...
auto area   = unit_dot(arm, arm);       // square_meters
auto moment = unit_cross(arm, force);   // a vector in newton_meters (torque)

// A dimensionless rotation / direction-cosine matrix applied to a dimensioned vector:
Eigen::Matrix<double, 3, 3>          rotation = /* ... */;
Eigen::Matrix<meters<double>, 3, 1>  rotated  = unit_transform(rotation, position);

The full helper set (unit_dot, unit_squared_norm, unit_norm, unit_normalized, unit_cross, unit_transform), the capability table, and the caveats are documented in the Eigen how-to.


Serialization

The opt-in header <units/serialization.h> encodes a quantity to a compact binary stream that carries its dimension along with its value. A reader recovers the quantity from the bytes alone — it discovers the dimension before it names a target type — so the two peers need no shared schema and no out-of-band agreement on the unit. The header is separate; <units.h> does not pull it in.

#include <units.h>
#include <units/serialization.h>
#include <fstream>
#include <iostream>

int main()
{
    using namespace units;
    using namespace units::literals;

    std::fstream file("speed.bin", std::ios::in | std::ios::out | std::ios::binary | std::ios::trunc);

    file << serialize(60.0_mph);               // write a quantity to any stream
    file.seekg(0);

    // read it back into the type you want — one call, dimension-checked
    if (auto kph = deserialize<kilometers_per_hour<double>>(file))
        std::cout << kph->value() << " kph\n"; // 96.5606 kph
}

serialize returns an any_unit — a first-class value that owns its serialized bytes and behaves like a value type: it streams (<</>>), compares (==, and </> within a dimension), hashes (usable as an unordered_map key), and renders to text — to_string() names the dimension when the library knows it (100 m, 9.81 m s^-2), or to_string_raw() for the always-available name-free form. deserialize returns one too (wrapped in std::expected, since bad bytes can fail). Collapse an any_unit into a concrete quantity with to<Unit>() (checked, returns std::expected), assign_to(out) (mismatch-tolerant, assigns into an existing variable and returns whether it fit), try_to<Unit>() / unit_cast<Unit>() (throwing), or visit() (the canonical unit of the decoded dimension, no target named). deserialize<Unit>(bytes) is the typed fast path when the type is known.

Prefer the strongly-typed unit types; reach for any_unit only where you genuinely need type erasure. any_unit trades compile-time dimension checking for a runtime-carried dimension, so it is the right tool at a boundary that loses the static type — serialization and deserialization, wire transport, or a heterogeneous stream whose dimension is not known until runtime. Everywhere the static type is available, the concrete unit types are better: the dimension is checked at compile time and there is no runtime dispatch or throw. One consequence to keep in mind: a dimensionless any_unit (a bare scalar, or a ratio such as meters / meters) resolves under a default visit() to dimensionless, so a ratio-scale like percent cannot be recovered from the erased form — name it explicitly (to<percent<double>>()) if you need that scale back.

It also drops into byte interfaces with no cast. Away from a stream, an any_unit exposes a modern, type-safe byte view (bytes() → std::span<const std::byte>) and a C-interface pair (data() → const char*, size()) that feeds std::fwrite, a socket send, or memcpy directly:

any_unit q = serialize(position);

std::fwrite(q.data(), 1, q.size(), fp);         // C stdio — no cast
::send(sock, q.data(), q.size(), 0);            // const char* decays to const void*

auto same = deserialize(q);                     // an any_unit converts to a span — round-trips directly

The stream identifies each base dimension by an 8-byte hash of its name, so the format has no fixed set of dimensions and no ceiling on how many a quantity composes: any base dimension round-trips, including one you define with make_dimension<>, with no central registry and no reflection. Values ride in SI canonical base in the tersest exact encoding (integer varint, 32-bit float, or 64-bit double), so a single-term integer quantity is a handful of bytes.

Bytes per serialized quantity across a spread, beside a naive {"value":V,"unit":"U"} JSON string for the same quantity (the JSON needs both peers to agree on the unit out of band; the binary carries the dimension itself):

QuantitySerialized bytesNaive JSON string
100.0_m1424
5000.0_g (5 kg)1323
1.0_GB1723
20.0_degC2026
60.0_mph2925
dimensionless<double>(0.25)7—

Full guide, wire format, error model, and measured compile-time and run-time numbers: Serialization.


Formatting with std::format

Every quantity works with std::format, std::print, std::println, and std::format_to out of the box — no extra include beyond <units.h> (or the relevant dimension header). With no format-spec you get the same text operator<< and to_string() produce:

#include <format>
#include <print>
#include <units.h>
using namespace units::literals;

std::println("{}", 3.5_m);          // 3.5 m
std::string s = std::format("{}", 9.81_mps_sq);   // 9.81 m s^-2  (dimension form for an unnamed unit)

The format grammar

{: value-spec [ % unit-opts ] }

Everything before an optional % is the value-spec and is forwarded verbatim to the underlying number's own std::formatter, so the entire standard numeric grammar applies (precision, width, fill/align, sign, #, 0, L, and type). Everything after the % are unit-opts (any order): at most one label-form flag, at most one show flag, and an optional quoted separator literal.

OptEffect
athe unit's own abbreviated label (default); base-dimension list if the unit is unnamed. Never converts the value.
nthe unit's own full name; base-dimension list if the unit is unnamed. Never converts the value.
bconvert the value and label to SI base units (6 ft → 1.8288 m, 2 km → 2000 m)
vvalue only — suppress the unit label
uunit only — suppress the value and separator
'…'separator literal between value and label (default is one space); \t \n \\ \' escapes; '' is empty

Only %b converts. %a and %n always show the value exactly as stored, in the unit's own symbol/name — there is no lossy "force the dimension symbols onto the stored number" mode, because a unit's identity (feet vs meters) is flattened to a single ratio at the type level and cannot be recovered as its own dimensional symbols. To normalize to SI, use %b.

An invalid spec is a compile error for a literal format string, or a thrown std::format_error for a runtime (std::vformat) string.

Worked examples

Every row is produced by the actual formatter (the library's docs examples are compiled, so they cannot drift from what the code does):

Format stringArgumentResult
{}3.5_m3.5 m
{:.2f}3.5_m3.50 m
{:.0f}3.5_m4 m
{:>10.2f}3.5_m⟨6 spaces⟩3.50 m
{:<10.2f}3.5_m3.50⟨7 spaces⟩m
{:^10.2f}3.5_m⟨3⟩3.50⟨4⟩m
{:*>10.2f}3.5_m******3.50 m
{:+.1f}3.5_m+3.5 m
{: .1f}3.5_m␣3.5 m
{:e}3.5_m3.500000e+00 m
{:d}meters<int>(255)255 m
{:#x}meters<int>(255)0xff m
{:#06x}meters<int>(255)0x00ff m
{:b}meters<int>(255)11111111 m
{:%a}3.5_m3.5 m
{:%a}6.0_ft6 ft (never converts)
{:%n}3.5_m3.5 meters
{:%n}6.0_ft6 feet
{:%b}6.0_ft1.8288 m (converted to SI)
{:%b}kilometers<>(2)2000 m
{:.4f%b}10.0_fps3.0480 m s^-1
{}9.81_mps9.81 mps
{}meters<>(6)/(seconds<>(2)*seconds<>(1))3 mps2
{:%v}3.5_m3.5
{:%u}3.5_mm
{:.2f%v}3.5_m3.50
{:%a''}3.5_m3.5m
{:%a'_'}3.5_m3.5_m
{:%a' - '}3.5_m3.5 - m
{:.2f%n'_'}3.5_m3.50_meters

Disabling text features

Text support is on by default. It is opt-out, via three macros (each also a CMake option, all default OFF), chosen to preserve backward compatibility for embedded builds:

Macro / CMake optionEffect
UNIT_LIB_DISABLE_IOSTREAM / UNITS_DISABLE_IOSTREAMDrops the stream inserters. For backward compatibility this also drops to_string, <string>, and std::format — a legacy iostream-disabled build has always been the lean, string-free build, and stays byte-for-byte that.
UNIT_LIB_DISABLE_FORMAT / UNITS_DISABLE_FORMATDrops only std::format support; iostream and to_string remain.
UNIT_LIB_DISABLE_STRING / UNITS_DISABLE_STRINGThe leanest build: forbids <string>, and therefore implies both of the above.

To keep std::format while dropping the iostream inserters, define UNIT_LIB_DISABLE_IOSTREAM and UNIT_LIB_ENABLE_FORMAT. std::format support additionally requires the standard library to provide <format>.


Integration

units is header-only.

Copy the headers. Put include/ on your include path and compile with C++23 (-std=c++23 on GCC and Clang). Nothing to build.

CMake — add_subdirectory. Vendor the project and link the interface target:

add_subdirectory(units)
target_link_libraries(myapp PRIVATE units::units)

CMake — FetchContent. Pull it at configure time:

include(FetchContent)
FetchContent_Declare(units
  GIT_REPOSITORY https://github.com/nholthaus/units.git
  GIT_TAG        v3.6.1)
FetchContent_MakeAvailable(units)
target_link_libraries(myapp PRIVATE units::units)

CMake — installed package. After installing (or from a Linux package), consume it with find_package:

find_package(units CONFIG REQUIRED)
target_link_libraries(myapp PRIVATE units::units)

Package managers. units is available through vcpkg (vcpkg install units), Conan (conan install --requires=units/<version>), and Debian/Ubuntu (apt install libunits-dev; an RPM and a tarball are also produced via CPack, and a PPA is published for Ubuntu). The reference sources for all three integrations live in packaging/.

Debugger visualizers show a quantity as 5 m in the debugger rather than an opaque object: linking units::units attaches the natvis automatically on MSVC, and an LLDB formatter (command script import units_lldb.py) does the same for LLDB, CLion, Xcode, and CodeLLDB.


Cheat sheet

The everyday API. Every line compiles under C++23; assumes using namespace units; and using namespace units::literals;. The full version, with more detail, is docs/reference/cheat-sheet.md.

// Make a quantity
meters a(5.0);            // CTAD -> meters<double>
meters b = 5.0_m;         // literal
auto   c = 5.0 * m;       // scalar * unit constant (units::m)
meters<int> f(5);         // explicit integer representation
// 5_m is meters<int>, 5.0_m is meters<double>: write the dot for fractional values.

// Convert (implicit, lossless only)
meters mm = 100.0_ft;     // feet -> meters
feet   ff = mm;           // meters -> feet
// meters<int> x = 1.0_ft;   // ERROR: lossy into an integer representation
constexpr bytes<int> two = 16_b;         // OK: exact at compile time (16 bits == 2 bytes)
// constexpr bytes<int> bad = 17_b;      // ERROR: not a whole number of bytes (never silently truncated)
bytes<int> n = units::floor<bytes<int>>(runtimeBits); // runtime lossy: floor/ceil/round/trunc<To>

// Arithmetic (dimensions are tracked)
square_meters     area  = 15.0_m * 5.0_m;    // m * m -> area
meters_per_second speed = 60.0_mi / 1.0_hr;  // -> velocity
// meters bad = 15.0_m * 5.0_m;   // ERROR: that product is an area, not a length
// auto z = 1.0_m + 1.0_s;        // ERROR: incompatible dimensions
// auto w = 1.0_m + 5.0;          // ERROR: scalar + dimensioned quantity

// Get a plain number out (no implicit unit -> double, except dimensionless)
double v = a.value();      // value in the quantity's units
double r = a.raw();        // stored value (differs from value() for percent/ppm)
double t = a.to<double>(); // explicit cast to a representation
int    i = a.to<int>();    // explicit, truncates toward zero

// Compare (same dimension only)
bool lt = (1.0_m < 2.0_ft);
// bool bad = (1.0_m < 1.0_kg);   // ERROR: incomparable dimensions

// Math — unqualified, found by ADL
meters h = sqrt(pow<2>(3.0_m) + pow<2>(4.0_m));   // 5 m
auto   s = sin(90.0_deg);          // trig needs an angle
// auto bad = sin(1.0_m);          // ERROR: sin needs an angle, not a length

// Name, print
std::cout << 5.0_m;                 // "5 m"
const char* n = (5.0_m).name();     // "meters"

// Dimensionless <-> arithmetic is implicit
dimensionless<double> ratio = 0.25; // double -> dimensionless
double back = ratio;                // dimensionless -> double
double frac = (50.0_pct).value();   // 0.5   (percent: .value() is the fraction)
double pts  = (50.0_pct).raw();     // 50    (.raw() is the point count)

// Constrain your own templates with the concepts
template <units::UnitType U> U twice(U x) { return x + x; }

// Constrain on a physical quantity by DIMENSION — every dimension emits a
// PascalCase concept (Velocity, Force, Length, Frequency, ...). Being
// dimension-keyed, these classify a computed result the same way in every
// translation unit, regardless of which dimension headers it included.
void report(units::Velocity auto v);              // any velocity
template <units::Force F> F clamp_force(F f);     // any force

// Define a unit in one line
namespace units {
    UNIT_ADD(length, smoots, smoot, conversion_factor<std::ratio<17018, 10000>, meters<>>)
}
auto bridge = 364.4_smoot;          // now a usable length, with its own literal

Supported units

Every built-in unit, by dimension — 48 dimensions, ~200 named units before metric prefixes. A unit marked yes under Prefixes also provides every SI metric prefix from femto to peta (_km, _mm, …). For a name shared across dimensions (e.g. pounds), qualify it: units::mass::pounds vs units::force::pounds. This table is generated from the headers by docs/reference/gen_reference.py.

acceleration

UnitLiteralPrefixes
meters_per_second_squared_mps2
feet_per_second_squared_fps2
standard_gravity_SG
gals_Gal

angle

UnitLiteralPrefixes
radians_radyes
degrees_deg
arcminutes_arcmin
arcseconds_arcsec
milliarcseconds_mas
turns_tr
gradians_gon
angular_mils_amil
compass_points_cpt

angular acceleration

UnitLiteralPrefixes
radians_per_second_squared_rad_per_s_sqyes
degrees_per_second_squared_deg_per_s_sq
gradians_per_second_squared_gon_per_s_sq
revolutions_per_second_squared_rev_per_s_sq
revolutions_per_minute_squared_rev_per_min_sq

angular jerk

UnitLiteralPrefixes
radians_per_second_cubed_rad_per_s_cuyes
degrees_per_second_cubed_deg_per_s_cu
gradians_per_second_cubed_gon_per_s_cu
revolutions_per_second_cubed_rev_per_s_cu
revolutions_per_minute_cubed_rev_per_min_cu

angular velocity

UnitLiteralPrefixes
radians_per_second_rad_per_s
degrees_per_second_deg_per_s
revolutions_per_minute_rpm
revolutions_per_second_rps
milliarcseconds_per_year_mas_per_yr

area

UnitLiteralPrefixes
square_meters_m2
square_feet_ft2
square_inches_in2
square_miles_mi2
square_kilometers_km2
hectares_ha
acres_acre
roods_rood
square_rods_rd2

capacitance

UnitLiteralPrefixes
farads_Fyes

charge

UnitLiteralPrefixes
coulombs_Cyes
ampere_hours_Ahyes
abcoulombs_abC
statcoulombs_statC

concentration

UnitLiteralPrefixes
parts_per_million_ppm
parts_per_billion_ppb
parts_per_trillion_ppt
percent_pct

conductance

UnitLiteralPrefixes
siemens_Syes

current

UnitLiteralPrefixes
amperes_Ayes
abamperes_abA
statamperes_statA

data

UnitLiteralPrefixes
bytes_B
kilobytes_kB
megabytes_MB
gigabytes_GB
terabytes_TB
petabytes_PB
exabytes_EB
kibibytes_KiB
mebibytes_MiB
gibibytes_GiB
tebibytes_TiB
pebibytes_PiB
exbibytes_EiB
bits_b
kilobits_kb
megabits_Mb
gigabits_Gb
terabits_Tb
petabits_Pb
exabits_Eb
kibibits_Kib
mebibits_Mib
gibibits_Gib
tebibits_Tib
pebibits_Pib
exbibits_Eib
nibbles_nibble

data transfer rate

UnitLiteralPrefixes
bytes_per_second_Bps
exabytes_per_second_EBps
bits_per_second_bps
exabits_per_second_Ebps

density

UnitLiteralPrefixes
kilograms_per_cubic_meter_kg_per_m3
grams_per_milliliter_g_per_mL
kilograms_per_liter_kg_per_L
ounces_per_cubic_foot_oz_per_ft3
ounces_per_cubic_inch_oz_per_in3
ounces_per_gallon_oz_per_gal
pounds_per_cubic_foot_lb_per_ft3
pounds_per_cubic_inch_lb_per_in3
pounds_per_gallon_lb_per_gal
slugs_per_cubic_foot_slug_per_ft3

energy

UnitLiteralPrefixes
joules_Jyes
calories_calyes
kilowatt_hours_kWh
watt_hours_Wh
british_thermal_units_BTU
british_thermal_units_iso_BTU_iso
british_thermal_units_59_BTU59
therms_thm
foot_pounds_ftlbf
ergs_erg
calories_it_cal_it
tons_of_tnt_tTNT

energy density

UnitLiteralPrefixes
joules_per_meter_cubed_J_per_m3yes

force

UnitLiteralPrefixes
newtons_Nyes
pounds_lbf
dynes_dyn
kiloponds_kp
poundals_pdl
kips_kip
ounces_force_ozf
grams_force_gf
short_tons_force_tonf
long_tons_force_ltonf
sthenes_sn

frequency

UnitLiteralPrefixes
hertz_Hzyes

illuminance

UnitLiteralPrefixes
lux_lxyes
footcandles_fc
lumens_per_square_inch_lm_per_in2
phots_ph

impedance

UnitLiteralPrefixes
ohms_Ohmyes

inductance

UnitLiteralPrefixes
henries_Hyes

irradiance

UnitLiteralPrefixes
watts_per_meter_squared_W_per_m2yes

jerk

UnitLiteralPrefixes
meters_per_second_cubed_mps3yes
feet_per_second_cubed_fps3

length

UnitLiteralPrefixes
meters_myes
feet_ft
inches_in
mils_mil
miles_mi
nautical_miles_nmi
astronomical_units_au
lightyears_ly
parsecs_pc
angstroms_angstrom
cubits_cbt
fathoms_ftm
chains_ch
furlongs_fur
hands_hand
leagues_lea
nautical_leagues_nl
yards_yd
rods_rod
links_li
barleycorns_bc
nails_nail
spans_span
picas_pica
points_pnt

luminance

UnitLiteralPrefixes
candelas_per_square_meter_cd_per_m2yes
stilbs_sb
apostilbs_asb
brils_bril
skots_sk
lamberts_la
millilamberts_mla
foot_lamberts_ftL

luminous flux

UnitLiteralPrefixes
lumens_lmyes

luminous intensity

UnitLiteralPrefixes
candelas_cdyes

magnetic field strength

UnitLiteralPrefixes
teslas_Teyes
gauss_G

magnetic flux

UnitLiteralPrefixes
webers_Wbyes
maxwells_Mx

mass

UnitLiteralPrefixes
grams_gyes
tonnes_t
pounds_lb
long_tons_ln_conversion_factor
short_tons_sh_conversion_factor
stone_st
ounces_oz
carats_ct
slugs_slug
grains_gr
avoirdupois_drams_dr_av
pennyweights_dwt
troy_ounces_ozt
troy_pounds_lbt
hundredweights_cwt
short_hundredweights_sh_cwt

power

UnitLiteralPrefixes
watts_Wyes
horsepower_hp
metric_horsepower_hpM
electrical_horsepower_hpE
tons_of_refrigeration_TR

pressure

UnitLiteralPrefixes
pascals_Payes
bars_bar
millibars_mbar
atmospheres_atm
pounds_per_square_inch_psi
torrs_torr
millimeters_of_mercury_mmHg
inches_of_mercury_inHg
technical_atmospheres_at
pounds_per_square_foot_psf
kips_per_square_inch_ksi
baryes_Ba
piezes_pz
centimeters_of_water_cmH2O
millimeters_of_water_mmH2O
inches_of_water_inH2O

radiance

UnitLiteralPrefixes
watts_per_steradian_per_meter_squared_W_per_srm2yes

radiant intensity

UnitLiteralPrefixes
watts_per_steradian_W_per_sryes

radiation

UnitLiteralPrefixes
becquerels_Bqyes
grays_Gyyes
sieverts_Svyes
curies_Ci
rutherfords_rd
radiation_absorbed_dose_rads
roentgens_equivalent_man_rem

solid angle

UnitLiteralPrefixes
steradians_sryes
degrees_squared_deg2
spats_sp

spectral flux

UnitLiteralPrefixes
watts_per_meter_W_per_myes

spectral intensity

UnitLiteralPrefixes
watts_per_steradian_per_meter_W_per_srmyes

spectral irradiance

UnitLiteralPrefixes
watts_per_meter_cubed_W_per_m3yes

spectral radiance

UnitLiteralPrefixes
watts_per_steradian_per_meter_cubed_W_per_srm3yes

substance

UnitLiteralPrefixes
mols_molyes
pound_moles_lbmol

substance concentration

UnitLiteralPrefixes
molars_Myes

substance mass

UnitLiteralPrefixes
grams_per_mole_g_per_molyes

temperature

UnitLiteralPrefixes
kelvin_K
celsius_degC
fahrenheit_degF
reaumur_Re
rankine_Ra

time

UnitLiteralPrefixes
seconds_syes
minutes_min
hours_hr
days_d
weeks_wk
years_yr
julian_years_a_j
gregorian_years_a_g
fortnights_fn
decades_dec
centuries_cent
millennia_kyr

torque

UnitLiteralPrefixes
newton_meters_Nm
pound_feet_lbf_ft
foot_poundals_ftpdl
inch_pounds_inlb
meter_kilograms_mkgf

velocity

UnitLiteralPrefixes
meters_per_second_mps
feet_per_second_fps
miles_per_hour_mph
kilometers_per_hour_kph
knots_kts
feet_per_minute_fpm
meters_per_minute_mpm
inches_per_second_ips
kilometers_per_second_kmps

viscosity

UnitLiteralPrefixes
pascal_seconds_Pa_s
poise_P
centipoise_cP
square_meters_per_second_m2_per_s
stokes_St
centistokes_cSt

voltage

UnitLiteralPrefixes
volts_Vyes
statvolts_statV
abvolts_abV

volume

UnitLiteralPrefixes
cubic_meters_m3
cubic_millimeters_mm3
cubic_kilometers_km3
liters_Lyes
cubic_inches_in3
cubic_feet_ft3
cubic_yards_yd3
cubic_miles_mi3
gallons_gal
quarts_qt
pints_pt
cups_c
fluid_ounces_fl_oz
barrels_bl
bushels_bu
cords_cord
cubic_fathoms_fm3
tablespoons_tbsp
teaspoons_tsp
pinches_pinch
dashes_dash
drops_drop
fifths_fifth
drams_dr
gills_gi
pecks_pk
sacks_sck
shots_shts
strikes_strk

volume flow rate

UnitLiteralPrefixes
cubic_meters_per_second_m3_per_s
cubic_meters_per_hour_m3_per_hr
liters_per_second_L_per_s
liters_per_minute_L_per_min
gallons_per_minute_gpm
gallons_per_hour_gph
cubic_feet_per_second_cfs
cubic_feet_per_minute_cfm

Physical constants

Provided in units::constants as typed quantities (each carries its dimension, so it participates in dimensional analysis). Values are the 2018 CODATA recommended values.

SymbolConstantValue
piRatio of a circle's circumference to its diameter1
cSpeed of light in vacuum299792458.0
GNewtonian constant of gravitation6.67430e-11
hPlanck constant6.62607015e-34
h_barReduced Planck constant1.054571817e-34
mu0vacuum permeability1.25663706212e-6
epsilon0vacuum permittivity8.8541878128e-12
Z0characteristic impedance of vacuum376.730313668
k_eCoulomb's constant8.9875517923e9
eelementary charge1.602176634e-19
m_eelectron mass9.1093837015e-31
m_pproton mass1.67262192369e-27
mu_BBohr Magneton9.2740100783e-24
N_AAvogadro's Number6.02214076e23
RGas constant8.314462618
k_BBoltzmann constant1.380649e-23
FFaraday constant96485.33212
sigmaStefan-Boltzmann constant5.670374419e-8

More capabilities

Beyond the catalog: unit-aware <cmath> (found by ADL), std::chrono::duration interop, std::hash and std::numeric_limits specializations, NaN/infinity support, self-describing binary serialization, optional nlohmann/json serialization, a concept vocabulary (UnitType, ConversionFactorType, …) plus a per-dimension concept for every dimension (Velocity, Force, Length, …) so you can constrain a template on a physical quantity by dimension (void f(Velocity auto v)), non-linear (decibel) scales, and affine temperature. Each has a how-to or reference page under docs/.


Documentation

The manual is under docs/ (hub: docs/README.md). The generated API reference is published at https://nholthaus.github.io/units/.

Learn

Explain

How-to

Reference

Meta


Citing

If you use units in academic or published work, a citation is appreciated. The repository includes a CITATION.cff, so GitHub's "Cite this repository" button (top right of the repository page) generates a formatted citation and BibTeX for you. A BibTeX entry:

@software{holthaus_units,
  author  = {Holthaus, Nic},
  title   = {units: a compile-time C++ dimensional-analysis and unit-conversion library},
  url     = {https://github.com/nholthaus/units},
  license = {MIT}
}

License

units is distributed under the MIT License. Copyright © Nic Holthaus.

compile-time
cpp14
dimensional-analysis
header-only
no-dependencies
template-metaprogramming
unit-conversion

nholthaus/units

a compile-time, header-only, dimensional analysis and unit conversion library built on c++14 with no dependencies.

C++

1,059

679 commits

updated Sep 18, 2026

See the code

README

units

A compile-time, header-only, dimensional-analysis and unit-conversion library for C++23, with no dependencies.

Windows build GCC build Clang build license copyright standard

units represents physical quantities as types. A quantity is a value with a unit — meters, feet, seconds — that behaves like the number it wraps. Conversions between compatible units are implicit and resolved at compile time; expressions that are dimensionally inconsistent do not compile.

Quantities are written with unit literals (5.0_m) or by multiplying a value by a unit constant (60.0 * km):

#include <units/length.h>
#include <iostream>

int main()
{
    using namespace units;
    using namespace units::literals;

    meters a = 5.0_m;        // unit literal
    meters b = 60.0 * km;    // value times a unit constant (== 60000 m)
    feet   c = a;            // implicit, lossless conversion

    std::cout << a << ", " << b << ", " << c << '\n';   // prints: 5 m, 60000 m, 16.4042 ft
}

units favors syntax that reads as ordinary code: quantities are written and combined the way you would write them by hand, so the common cases are apparent from the code without consulting the reference.

Every snippet in this README and in the documentation is compiled and run as part of the test suite — see examples/.

Design

The library is organized around syntax that reads as ordinary arithmetic. A quantity is constructed with a unit literal (5.0_m) or a unit constant (60.0 * km), combined with the usual operators (+, *, /, comparisons), converted by assignment, and printed with <<. The common operations are intended to work as written; the deeper machinery (class-based named types, CTAD, ADL) exists so that this surface stays small and the code stays legible.

Contents


Features

  • Syntax. Quantities are written as meters, 60_mi / 1_hr, sqrt(area); operations are performed on the quantity types.
  • Batteries included. Over 280 units across 48 dimensions ship ready to use — SI, imperial and US-customary, ancient, and esoteric alike (3.0_cwt, 10.0_fur, 1.0_rem), each a named type with a literal and an exact, canonically-sourced conversion ratio. There is no unit "system" to select or instantiate: every unit is first class and used directly, and units from different systems combine in one expression (1.0_m + 3.0_ft + 1.0_fur). Nothing to configure, no unit to define before use.
  • Run-time cost. Conversions are constexpr ratios; a conversion between equivalent representations compiles to no machine code. A quantity is a trivially-copyable value the size of its underlying type.
  • Dimensional checking. Adding a length to a time, or assigning an area to a length, is a compile error. The dimensional analysis is performed by the type system.
  • Decibel and logarithmic scales. A unit's scale is part of its type. Alongside the default linear scale, decibel_scale provides dBW, dBm, and the dimensionless dB, with scale-correct arithmetic — adding decibels multiplies the underlying linear quantities. A decibel-scale unit requires a floating-point underlying type.
  • Diagnostics. A dimensional error names the unit type (meters<double>) rather than the conversion_factor<...> template. See Type errors.
  • Linear algebra with Eigen. Store dimensioned quantities in Eigen vectors and matrices with the dimensions checked at compile time — Eigen::Matrix<meters<double>, 3, 1>. Optional and dependency-free (activates only if Eigen is present). See Linear algebra with Eigen.
  • Self-describing serialization. serialize(q) writes a quantity to a compact binary stream that carries its dimension as well as its value; a reader recovers it with no prior agreement on the type, and the format extends to any base dimension — including your own make_dimension<> — with no central table and no reflection. See Serialization.
  • Trivial integration. Header-only, no dependencies, one #include. Drop in the headers, or consume the CMake package. See Integration.

Requirements

units requires a C++23 compiler. It is continuously tested on:

CompilerVersionPlatform
GCC (g++)13Ubuntu (latest)
Clang (clang++)19Ubuntu (latest)
MSVC (Visual Studio)2022Windows Server 2022
MSVC (Visual Studio)2026Windows Server 2025

Older toolchains are not supported by the 3.x line. The last release for the C++14 era is the 2.x series (see Migrating from 2.x).


Getting started

This section covers what most code needs. The full manual has the rest.

Include a header, and bring in the literal operators. Include the umbrella header <units.h> for every dimension, or one per-dimension header (<units/length.h>, <units/time.h>, …) for just the dimensions you use:

#include <units.h>            // everything; or <units/length.h>, <units/velocity.h>, ... for a subset
using namespace units;
using namespace units::literals;   // the _m, _s, _kg, ... literals

Note — if compiles are slow, include less. <units.h> pulls in all 48 dimensions. The library is heavily templated, so a translation unit's compile time scales with how much it instantiates; including only the per-dimension headers you use keeps it down. As a best practice — not a requirement — include the dimension a computed result lands in (dividing a length by a time yields a velocity, so <units/velocity.h> lets you name and print that result as meters_per_second); a translation unit that omits it still computes the correct value and dimension, just under the plain unit<...> type. Run-time behavior and code size are unaffected either way.

Make a quantity. Four equivalent forms:

meters a(5.0);          // construction (CTAD deduces meters<double>)
meters b = 5.0_m;       // a unit literal
meters c = 5.0 * m;     // a value times a unit constant (units::m)
meters d{5.0};          // braced construction

Note — unit literals are floating-point. Both 5.0_m and 5_m are meters<double>, so 1_m / 2_m is 0.5, not 0 — a literal never silently does integer arithmetic. An integer representation is opt-in through the type (meters<int> n{5}, or CTAD from an integer initializer meters a(5)); a compile-time value that is exact narrows into it (meters<int> n = 5_m;), while a fractional one (5.5_m) is a compile error rather than a silent truncation.

Spelling the type: meters, meters<>, meters<T>. Three ways to name the type:

  • meters<T> — an explicit representation (meters<double>, meters<float>, meters<int>). Valid as a variable, function parameter, return type, or member.
  • meters<> — the default representation; identical to meters<double>. Valid in the same positions.
  • meters — the bare name deduces the representation from the initializer (CTAD): meters a(5.0) is meters<double>, meters a(5) is meters<int>. Valid only where an initializer is present to deduce from — a local variable. A function parameter, a return type, and a class member have no initializer, so they require meters<> or meters<T>.
meters        local(5.0);      // bare name, deduced meters<double>
meters<>      m;               // default representation
meters<float> as_float(5.0f);  // explicit representation
// void f(meters q);           // ill-formed: a parameter has no initializer to deduce from
// meters make();              // ill-formed: a return type has no initializer to deduce from

auto vs. an explicit type. Use auto on the left when the right-hand side already states the unit:

auto d = 5.0_m;                  // meters
auto speed = 60.0_mi / 1.0_hr;   // a velocity

Write the type explicitly on the left when the compiler should confirm the dimensional analysis: naming the result type makes a mismatch a compile error rather than an accepted auto deduction.

square_meters     area  = 15.0_m * 5.0_m;    // the result is an area
meters_per_second speed = 100.0_m / 8.0_s;   // the result is a velocity
// meters bad = 15.0_m * 5.0_m;              // ill-formed: the result is an area, not a length

Convert by assigning between compatible units (implicit, and only when lossless):

meters m = 100.0_ft;    // feet -> meters
feet   f = m;           // meters -> feet

Do arithmetic — the result carries the correct dimension; name it and the compiler checks it:

square_meters     area  = 15.0_m * 5.0_m;    // m * m -> area
meters_per_second speed = 60.0_mi / 1.0_hr;   // -> velocity
meters            side  = sqrt(area / 3.0);   // <cmath> functions are unit-aware (found by ADL)

Get a plain number back out at the boundary with non-units code (there is no implicit quantity → double, except for dimensionless quantities):

double v = speed.value();     // the value in the quantity's units
double t = speed.to<double>();// cast to a chosen representation
std::cout << speed;           // or print it directly: "26.8224 mps"

That is enough for most use cases. How meters a(5.0) deduces its type and why sqrt needs no units:: prefix are covered in CTAD and ADL; the full walkthrough is in Getting started.

Temperatures and other affine units

A few units are affine: they carry a datum offset, not just a scale. Degrees Celsius and Fahrenheit are the common examples — 0 °C is 273.15 K, not 0 K. An affine quantity is an absolute point on a scale, and a difference of two points is a delta (an amount of temperature, with no datum). The library keeps this distinction quiet — you write ordinary arithmetic and it does the physically correct thing — but the rules are worth knowing:

using namespace units::temperature;

// point − point → a delta (the datum offsets cancel):
auto d = celsius<double>(100.0) - fahrenheit<double>(32.0);   // 100 K  (a temperature difference)
kelvin<double>(celsius<double>(0.0) - kelvin<double>(0.0));   // 273.15 K

// point ± delta → move the point (compound assignment reads the rhs as a relative amount):
celsius<double> t(20.0);
t += celsius<double>(5.0);   // warm by 5 degrees → 25 °C   (still an absolute temperature)
t -= celsius<double>(10.0);  // cool by 10 degrees → 15 °C

// point + point is disabled for OFFSET scales — summing two degrees-Celsius values adds their datums,
// which is arithmetically corrupt (20 °C + 5 °C is not 25 °C in any absolute sense):
// auto bad = celsius<double>(20.0) + celsius<double>(5.0);   // does not compile (by design)
// Zero-offset scales (kelvin, rankine) have no datum to corrupt, so they add like any linear unit.

Comparisons (==, <, …) and conversions between affine units are exact and always available. Non-affine units (lengths, masses, everything without an offset) are unaffected — they add, subtract, and combine with no special cases.

Opt-in absolute<> / delta<> wrappers

The point/delta distinction above is enforced quietly on plain units. When you want the compiler to enforce it in the type — so a function that takes a temperature difference cannot be handed an absolute temperature, or an epoch cannot be added to an epoch — reach for the two opt-in wrappers.

They live in their own header, so they are strictly opt-in: nothing about them exists until you ask for it.

#include <units/kind.h>   // the wrappers exist only where you include this

Without that include (even with <units.h>) there is no absolute, no delta, no kind — so your own names by those spellings are never disturbed, and there is no cost. Once included, the two wrappers are:

  • absolute<U> — a point on a scale. It carries the unit's datum: absolute<celsius<double>>(0.0) is 273.15 K, and .to<kelvin<double>>() applies the offset.
  • delta<U> — an amount (offset-free). delta<celsius<double>>(10.0).to<fahrenheit<double>>() is an 18 °F delta (scale only), never an absolute 50 °F.

They wrap any unit (for a non-affine unit the datum is zero, so the two coincide numerically), are trivially copyable, and are exactly the size of the wrapped unit — zero overhead. The type algebra, enforced at compile time:

using namespace units;
using namespace units::temperature;

// point − point → delta (the datum cancels), kept in the LEFT operand's unit:
auto span = absolute<celsius<double>>(100.0) - absolute<fahrenheit<double>>(32.0);  // delta of 100 °C-degrees
static_assert(traits::is_delta_v<decltype(span)>);

// point ± delta → point (move the point), delta ± delta → delta, delta */ scalar → delta:
auto warmer = absolute<celsius<double>>(20.0) + delta<celsius<double>>(5.0);        // 25 °C (a point)
delta<celsius<double>> d(10.0);
d += delta<fahrenheit<double>>(18.0);   // +10 °C-degrees (mixed unit converts by degree size)
d *= 2.0;                               // 40 °C-degrees

// point + point is ill-formed — the sum of two points has no meaning:
// auto bad = absolute<celsius<double>>(20.0) + absolute<celsius<double>>(5.0);     // does not compile

Result unit — the LHS-unit tie-break. A wrapper arithmetic operator keeps the left operand's unit, so .value() reads in the unit you wrote: absolute<celsius> − absolute<fahrenheit> is a difference in celsius-degrees (reads 100), not a value in some common sub-unit. The only adjustment is to the underlying type: when the left operand's underlying is integral and cannot hold the right operand losslessly (e.g. absolute<kilometers<int>> − absolute<meters<int>>), the underlying promotes to floating point while the unit stays kilometers (the delta reads 0.5). Comparisons reconcile to the common (finer) unit so a mixed integer comparison never narrows.

Traits and concepts let a template constrain on the role: traits::is_absolute_v<T>, traits::is_delta_v<T>, and the concepts AbsoluteType / DeltaType. units::abs/min/max/clamp work on a delta; min/max/ clamp work on an absolute. Streaming and units::to_string forward to the wrapped quantity, prefixing a delta marker so a difference is visually distinct from a point.

One conversion verb, to<Target>(), crosses between types: a plain-unit target unwraps (a point applies its datum, a delta is scale-only), a wrapper target stays wrapped — c.to<kelvin<double>>() is a plain 273.15 K, c.to<absolute<kelvin<double>>>() stays a point. There is no .quantity() accessor; to<PlainUnit>() is the way out.

String-tagged kind<Tag, U> — same unit, different kind. When two quantities share a unit and a dimension yet are semantically different — a radial vs. a straight-line distance, a torque vs. an energy — tag each with a compile-time string so the types don't mix:

#include <units/kind.h>
using namespace units;
using namespace units::length;

kind<"radial",   meters<double>> r(5.0);
kind<"straight", meters<double>> s(3.0);
auto ok  = r + kind<"radial", meters<double>>(1.0);   // same kind → radial 6 m
// auto no = r + s;                                    // ill-formed: "cannot add two DIFFERENT kinds"

A plain unit is constructible into a kind by deliberate assignment but does not mix with one in arithmetic; a tag mismatch produces a readable, tag-naming diagnostic. absolute/delta live in an inline namespace affine, so units::absolute and units::affine::absolute name the same type — qualify only to disambiguate.

See the how-to guide absolute & delta wrappers for the full type algebra, the datum rules, kind<>, and worked examples, and affine temperature for the why.

Serialization of the wrappers is not yet supported (a follow-up); unwrap with to<PlainUnit>() and serialize that.


Type errors

A dimensional mistake that a bare double would accept is rejected at compile time, and the diagnostic names the unit type. The messages below are captured verbatim from GCC 13.

Adding incompatible dimensions:

readable_add_incompatible.cpp:9:18: error: no match for ‘operator+’ (operand types are ‘units::length::meters<double>’ and ‘units::time::seconds<double>’)
    9 | auto bad = 1.0_m + 1.0_s; // ill-formed: cannot add length and time
      |            ~~~~~ ^ ~~~~~
      |            |       |
      |            |       units::time::seconds<double>
      |            units::length::meters<double>

Assigning a product to the wrong dimension — m * m is an area, not a length. GCC reports the result through an internal alias with the named type beside it in {aka …}:

readable_wrong_result_type.cpp:10:41: error: conversion from ‘units::detail::rewrap_to_named_t<units::unit<units::area::square_meters_, double, units::linear_scale> >’ {aka ‘units::area::square_meters<double>’} to non-scalar type ‘units::length::meters<double>’ requested
   10 | units::length::meters<double> a = 1.0_m * 1.0_m; // ill-formed: m*m is an area, not a length
      |                                   ~~~~~~^~~~~~~

The full set of rejected operations, with the diagnostic each produces on GCC, Clang, and MSVC, is in Type safety. The diagnostics there are captured from the compilers by the test harness.


Run-time cost

A quantity is a trivially-copyable value the size of its underlying type; conversion ratios are constexpr. The type abstraction compiles away: the generated code matches hand-written double. The following disassembly is at -O2 (-O3 -march=x86-64-v3 for the loop); GCC 15 and Clang 21 agree.

A runtime expression compiles to the same instructions. Computing 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 — yields three floating-point instructions either way (a multiply, a divide, a multiply), differing only in operand order:

double         distance_raw  (double mph, double sec)                        { return (mph * 1609.344 / 3600.0) * sec; }
meters<double> distance_units(miles_per_hour<double> v, seconds<double> t)   { return v * t; }
distance_raw:                        distance_units:
    mulsd   .LC0(%rip), %xmm0            mulsd   %xmm1, %xmm0
    divsd   .LC1(%rip), %xmm0            mulsd   .LC2(%rip), %xmm0
    mulsd   %xmm1, %xmm0                 divsd   .LC3(%rip), %xmm0
    ret                                  ret

A conversion between equivalent representations is free. Passing a meters where a meters is wanted is not a cheap conversion — it is no conversion:

double roundtrip(meters<double> m) { meters<double> copy = m; return copy.value(); }
roundtrip:
    ret                              ; the whole function

A compile-time conversion is done by the compiler. A conversion of known values folds to a single constant load — the arithmetic never runs:

double speed_limit_mps() { return meters_per_second<double>(65.0_mph).value(); }
speed_limit_mps:
    movsd   .LC0(%rip), %xmm0        ; xmm0 = 29.0576  (65 mph, converted at compile time)
    ret

A hot loop vectorizes the same. Summing an array of kilometers as meters produces the identical instruction stream — including the AVX vectorization — as the raw-double loop; and

static_assert(1.0_km + 1.0_m == 1001.0_m);   // evaluated at compile time

holds with no run-time work. See Efficiency for the full comparison.


Linear algebra with Eigen

units composes with Eigen so you can carry dimensions through vectors and matrices — a rotated position, a moment computed from a lever arm and a force, a velocity integrated over a step — with the dimensional analysis still done by the type system, and with no dependency added to either library. The support activates automatically when <Eigen/Core> is on your include path and is a no-op when it is not (guarded by __has_include, exactly like the optional JSON support); there is no build flag to set.

#include <Eigen/Core>
#include <units.h>

using namespace units;
using namespace units::literals;

Eigen::Matrix<meters<double>, 3, 1> position;
position << 1.0_m, 2.0_m, 2.0_m;

auto           doubled = position * 2.0;       // scale — still a vector of meters
auto           sum     = position.sum();       // 5 m
meters<double> range   = unit_norm(position);  // 3 m — the norm returns to the original dimension

A vector holds one scalar type, so same-dimension operations — construction, +/-, scaling, sum(), block and Map views, cast() — work directly on Eigen expressions. The operations whose result changes dimension (a dot product of lengths is an area; a cross product carries the product dimension) are provided as helpers that compute the dimensionally-correct type:

#include <units/area.h>

Eigen::Matrix<meters<double>,  3, 1> arm;
Eigen::Matrix<newtons<double>, 3, 1> force;
// ...
auto area   = unit_dot(arm, arm);       // square_meters
auto moment = unit_cross(arm, force);   // a vector in newton_meters (torque)

// A dimensionless rotation / direction-cosine matrix applied to a dimensioned vector:
Eigen::Matrix<double, 3, 3>          rotation = /* ... */;
Eigen::Matrix<meters<double>, 3, 1>  rotated  = unit_transform(rotation, position);

The full helper set (unit_dot, unit_squared_norm, unit_norm, unit_normalized, unit_cross, unit_transform), the capability table, and the caveats are documented in the Eigen how-to.


Serialization

The opt-in header <units/serialization.h> encodes a quantity to a compact binary stream that carries its dimension along with its value. A reader recovers the quantity from the bytes alone — it discovers the dimension before it names a target type — so the two peers need no shared schema and no out-of-band agreement on the unit. The header is separate; <units.h> does not pull it in.

#include <units.h>
#include <units/serialization.h>
#include <fstream>
#include <iostream>

int main()
{
    using namespace units;
    using namespace units::literals;

    std::fstream file("speed.bin", std::ios::in | std::ios::out | std::ios::binary | std::ios::trunc);

    file << serialize(60.0_mph);               // write a quantity to any stream
    file.seekg(0);

    // read it back into the type you want — one call, dimension-checked
    if (auto kph = deserialize<kilometers_per_hour<double>>(file))
        std::cout << kph->value() << " kph\n"; // 96.5606 kph
}

serialize returns an any_unit — a first-class value that owns its serialized bytes and behaves like a value type: it streams (<</>>), compares (==, and </> within a dimension), hashes (usable as an unordered_map key), and renders to text — to_string() names the dimension when the library knows it (100 m, 9.81 m s^-2), or to_string_raw() for the always-available name-free form. deserialize returns one too (wrapped in std::expected, since bad bytes can fail). Collapse an any_unit into a concrete quantity with to<Unit>() (checked, returns std::expected), assign_to(out) (mismatch-tolerant, assigns into an existing variable and returns whether it fit), try_to<Unit>() / unit_cast<Unit>() (throwing), or visit() (the canonical unit of the decoded dimension, no target named). deserialize<Unit>(bytes) is the typed fast path when the type is known.

Prefer the strongly-typed unit types; reach for any_unit only where you genuinely need type erasure. any_unit trades compile-time dimension checking for a runtime-carried dimension, so it is the right tool at a boundary that loses the static type — serialization and deserialization, wire transport, or a heterogeneous stream whose dimension is not known until runtime. Everywhere the static type is available, the concrete unit types are better: the dimension is checked at compile time and there is no runtime dispatch or throw. One consequence to keep in mind: a dimensionless any_unit (a bare scalar, or a ratio such as meters / meters) resolves under a default visit() to dimensionless, so a ratio-scale like percent cannot be recovered from the erased form — name it explicitly (to<percent<double>>()) if you need that scale back.

It also drops into byte interfaces with no cast. Away from a stream, an any_unit exposes a modern, type-safe byte view (bytes() → std::span<const std::byte>) and a C-interface pair (data() → const char*, size()) that feeds std::fwrite, a socket send, or memcpy directly:

any_unit q = serialize(position);

std::fwrite(q.data(), 1, q.size(), fp);         // C stdio — no cast
::send(sock, q.data(), q.size(), 0);            // const char* decays to const void*

auto same = deserialize(q);                     // an any_unit converts to a span — round-trips directly

The stream identifies each base dimension by an 8-byte hash of its name, so the format has no fixed set of dimensions and no ceiling on how many a quantity composes: any base dimension round-trips, including one you define with make_dimension<>, with no central registry and no reflection. Values ride in SI canonical base in the tersest exact encoding (integer varint, 32-bit float, or 64-bit double), so a single-term integer quantity is a handful of bytes.

Bytes per serialized quantity across a spread, beside a naive {"value":V,"unit":"U"} JSON string for the same quantity (the JSON needs both peers to agree on the unit out of band; the binary carries the dimension itself):

QuantitySerialized bytesNaive JSON string
100.0_m1424
5000.0_g (5 kg)1323
1.0_GB1723
20.0_degC2026
60.0_mph2925
dimensionless<double>(0.25)7—

Full guide, wire format, error model, and measured compile-time and run-time numbers: Serialization.


Formatting with std::format

Every quantity works with std::format, std::print, std::println, and std::format_to out of the box — no extra include beyond <units.h> (or the relevant dimension header). With no format-spec you get the same text operator<< and to_string() produce:

#include <format>
#include <print>
#include <units.h>
using namespace units::literals;

std::println("{}", 3.5_m);          // 3.5 m
std::string s = std::format("{}", 9.81_mps_sq);   // 9.81 m s^-2  (dimension form for an unnamed unit)

The format grammar

{: value-spec [ % unit-opts ] }

Everything before an optional % is the value-spec and is forwarded verbatim to the underlying number's own std::formatter, so the entire standard numeric grammar applies (precision, width, fill/align, sign, #, 0, L, and type). Everything after the % are unit-opts (any order): at most one label-form flag, at most one show flag, and an optional quoted separator literal.

OptEffect
athe unit's own abbreviated label (default); base-dimension list if the unit is unnamed. Never converts the value.
nthe unit's own full name; base-dimension list if the unit is unnamed. Never converts the value.
bconvert the value and label to SI base units (6 ft → 1.8288 m, 2 km → 2000 m)
vvalue only — suppress the unit label
uunit only — suppress the value and separator
'…'separator literal between value and label (default is one space); \t \n \\ \' escapes; '' is empty

Only %b converts. %a and %n always show the value exactly as stored, in the unit's own symbol/name — there is no lossy "force the dimension symbols onto the stored number" mode, because a unit's identity (feet vs meters) is flattened to a single ratio at the type level and cannot be recovered as its own dimensional symbols. To normalize to SI, use %b.

An invalid spec is a compile error for a literal format string, or a thrown std::format_error for a runtime (std::vformat) string.

Worked examples

Every row is produced by the actual formatter (the library's docs examples are compiled, so they cannot drift from what the code does):

Format stringArgumentResult
{}3.5_m3.5 m
{:.2f}3.5_m3.50 m
{:.0f}3.5_m4 m
{:>10.2f}3.5_m⟨6 spaces⟩3.50 m
{:<10.2f}3.5_m3.50⟨7 spaces⟩m
{:^10.2f}3.5_m⟨3⟩3.50⟨4⟩m
{:*>10.2f}3.5_m******3.50 m
{:+.1f}3.5_m+3.5 m
{: .1f}3.5_m␣3.5 m
{:e}3.5_m3.500000e+00 m
{:d}meters<int>(255)255 m
{:#x}meters<int>(255)0xff m
{:#06x}meters<int>(255)0x00ff m
{:b}meters<int>(255)11111111 m
{:%a}3.5_m3.5 m
{:%a}6.0_ft6 ft (never converts)
{:%n}3.5_m3.5 meters
{:%n}6.0_ft6 feet
{:%b}6.0_ft1.8288 m (converted to SI)
{:%b}kilometers<>(2)2000 m
{:.4f%b}10.0_fps3.0480 m s^-1
{}9.81_mps9.81 mps
{}meters<>(6)/(seconds<>(2)*seconds<>(1))3 mps2
{:%v}3.5_m3.5
{:%u}3.5_mm
{:.2f%v}3.5_m3.50
{:%a''}3.5_m3.5m
{:%a'_'}3.5_m3.5_m
{:%a' - '}3.5_m3.5 - m
{:.2f%n'_'}3.5_m3.50_meters

Disabling text features

Text support is on by default. It is opt-out, via three macros (each also a CMake option, all default OFF), chosen to preserve backward compatibility for embedded builds:

Macro / CMake optionEffect
UNIT_LIB_DISABLE_IOSTREAM / UNITS_DISABLE_IOSTREAMDrops the stream inserters. For backward compatibility this also drops to_string, <string>, and std::format — a legacy iostream-disabled build has always been the lean, string-free build, and stays byte-for-byte that.
UNIT_LIB_DISABLE_FORMAT / UNITS_DISABLE_FORMATDrops only std::format support; iostream and to_string remain.
UNIT_LIB_DISABLE_STRING / UNITS_DISABLE_STRINGThe leanest build: forbids <string>, and therefore implies both of the above.

To keep std::format while dropping the iostream inserters, define UNIT_LIB_DISABLE_IOSTREAM and UNIT_LIB_ENABLE_FORMAT. std::format support additionally requires the standard library to provide <format>.


Integration

units is header-only.

Copy the headers. Put include/ on your include path and compile with C++23 (-std=c++23 on GCC and Clang). Nothing to build.

CMake — add_subdirectory. Vendor the project and link the interface target:

add_subdirectory(units)
target_link_libraries(myapp PRIVATE units::units)

CMake — FetchContent. Pull it at configure time:

include(FetchContent)
FetchContent_Declare(units
  GIT_REPOSITORY https://github.com/nholthaus/units.git
  GIT_TAG        v3.6.1)
FetchContent_MakeAvailable(units)
target_link_libraries(myapp PRIVATE units::units)

CMake — installed package. After installing (or from a Linux package), consume it with find_package:

find_package(units CONFIG REQUIRED)
target_link_libraries(myapp PRIVATE units::units)

Package managers. units is available through vcpkg (vcpkg install units), Conan (conan install --requires=units/<version>), and Debian/Ubuntu (apt install libunits-dev; an RPM and a tarball are also produced via CPack, and a PPA is published for Ubuntu). The reference sources for all three integrations live in packaging/.

Debugger visualizers show a quantity as 5 m in the debugger rather than an opaque object: linking units::units attaches the natvis automatically on MSVC, and an LLDB formatter (command script import units_lldb.py) does the same for LLDB, CLion, Xcode, and CodeLLDB.


Cheat sheet

The everyday API. Every line compiles under C++23; assumes using namespace units; and using namespace units::literals;. The full version, with more detail, is docs/reference/cheat-sheet.md.

// Make a quantity
meters a(5.0);            // CTAD -> meters<double>
meters b = 5.0_m;         // literal
auto   c = 5.0 * m;       // scalar * unit constant (units::m)
meters<int> f(5);         // explicit integer representation
// 5_m is meters<int>, 5.0_m is meters<double>: write the dot for fractional values.

// Convert (implicit, lossless only)
meters mm = 100.0_ft;     // feet -> meters
feet   ff = mm;           // meters -> feet
// meters<int> x = 1.0_ft;   // ERROR: lossy into an integer representation
constexpr bytes<int> two = 16_b;         // OK: exact at compile time (16 bits == 2 bytes)
// constexpr bytes<int> bad = 17_b;      // ERROR: not a whole number of bytes (never silently truncated)
bytes<int> n = units::floor<bytes<int>>(runtimeBits); // runtime lossy: floor/ceil/round/trunc<To>

// Arithmetic (dimensions are tracked)
square_meters     area  = 15.0_m * 5.0_m;    // m * m -> area
meters_per_second speed = 60.0_mi / 1.0_hr;  // -> velocity
// meters bad = 15.0_m * 5.0_m;   // ERROR: that product is an area, not a length
// auto z = 1.0_m + 1.0_s;        // ERROR: incompatible dimensions
// auto w = 1.0_m + 5.0;          // ERROR: scalar + dimensioned quantity

// Get a plain number out (no implicit unit -> double, except dimensionless)
double v = a.value();      // value in the quantity's units
double r = a.raw();        // stored value (differs from value() for percent/ppm)
double t = a.to<double>(); // explicit cast to a representation
int    i = a.to<int>();    // explicit, truncates toward zero

// Compare (same dimension only)
bool lt = (1.0_m < 2.0_ft);
// bool bad = (1.0_m < 1.0_kg);   // ERROR: incomparable dimensions

// Math — unqualified, found by ADL
meters h = sqrt(pow<2>(3.0_m) + pow<2>(4.0_m));   // 5 m
auto   s = sin(90.0_deg);          // trig needs an angle
// auto bad = sin(1.0_m);          // ERROR: sin needs an angle, not a length

// Name, print
std::cout << 5.0_m;                 // "5 m"
const char* n = (5.0_m).name();     // "meters"

// Dimensionless <-> arithmetic is implicit
dimensionless<double> ratio = 0.25; // double -> dimensionless
double back = ratio;                // dimensionless -> double
double frac = (50.0_pct).value();   // 0.5   (percent: .value() is the fraction)
double pts  = (50.0_pct).raw();     // 50    (.raw() is the point count)

// Constrain your own templates with the concepts
template <units::UnitType U> U twice(U x) { return x + x; }

// Constrain on a physical quantity by DIMENSION — every dimension emits a
// PascalCase concept (Velocity, Force, Length, Frequency, ...). Being
// dimension-keyed, these classify a computed result the same way in every
// translation unit, regardless of which dimension headers it included.
void report(units::Velocity auto v);              // any velocity
template <units::Force F> F clamp_force(F f);     // any force

// Define a unit in one line
namespace units {
    UNIT_ADD(length, smoots, smoot, conversion_factor<std::ratio<17018, 10000>, meters<>>)
}
auto bridge = 364.4_smoot;          // now a usable length, with its own literal

Supported units

Every built-in unit, by dimension — 48 dimensions, ~200 named units before metric prefixes. A unit marked yes under Prefixes also provides every SI metric prefix from femto to peta (_km, _mm, …). For a name shared across dimensions (e.g. pounds), qualify it: units::mass::pounds vs units::force::pounds. This table is generated from the headers by docs/reference/gen_reference.py.

acceleration

UnitLiteralPrefixes
meters_per_second_squared_mps2
feet_per_second_squared_fps2
standard_gravity_SG
gals_Gal

angle

UnitLiteralPrefixes
radians_radyes
degrees_deg
arcminutes_arcmin
arcseconds_arcsec
milliarcseconds_mas
turns_tr
gradians_gon
angular_mils_amil
compass_points_cpt

angular acceleration

UnitLiteralPrefixes
radians_per_second_squared_rad_per_s_sqyes
degrees_per_second_squared_deg_per_s_sq
gradians_per_second_squared_gon_per_s_sq
revolutions_per_second_squared_rev_per_s_sq
revolutions_per_minute_squared_rev_per_min_sq

angular jerk

UnitLiteralPrefixes
radians_per_second_cubed_rad_per_s_cuyes
degrees_per_second_cubed_deg_per_s_cu
gradians_per_second_cubed_gon_per_s_cu
revolutions_per_second_cubed_rev_per_s_cu
revolutions_per_minute_cubed_rev_per_min_cu

angular velocity

UnitLiteralPrefixes
radians_per_second_rad_per_s
degrees_per_second_deg_per_s
revolutions_per_minute_rpm
revolutions_per_second_rps
milliarcseconds_per_year_mas_per_yr

area

UnitLiteralPrefixes
square_meters_m2
square_feet_ft2
square_inches_in2
square_miles_mi2
square_kilometers_km2
hectares_ha
acres_acre
roods_rood
square_rods_rd2

capacitance

UnitLiteralPrefixes
farads_Fyes

charge

UnitLiteralPrefixes
coulombs_Cyes
ampere_hours_Ahyes
abcoulombs_abC
statcoulombs_statC

concentration

UnitLiteralPrefixes
parts_per_million_ppm
parts_per_billion_ppb
parts_per_trillion_ppt
percent_pct

conductance

UnitLiteralPrefixes
siemens_Syes

current

UnitLiteralPrefixes
amperes_Ayes
abamperes_abA
statamperes_statA

data

UnitLiteralPrefixes
bytes_B
kilobytes_kB
megabytes_MB
gigabytes_GB
terabytes_TB
petabytes_PB
exabytes_EB
kibibytes_KiB
mebibytes_MiB
gibibytes_GiB
tebibytes_TiB
pebibytes_PiB
exbibytes_EiB
bits_b
kilobits_kb
megabits_Mb
gigabits_Gb
terabits_Tb
petabits_Pb
exabits_Eb
kibibits_Kib
mebibits_Mib
gibibits_Gib
tebibits_Tib
pebibits_Pib
exbibits_Eib
nibbles_nibble

data transfer rate

UnitLiteralPrefixes
bytes_per_second_Bps
exabytes_per_second_EBps
bits_per_second_bps
exabits_per_second_Ebps

density

UnitLiteralPrefixes
kilograms_per_cubic_meter_kg_per_m3
grams_per_milliliter_g_per_mL
kilograms_per_liter_kg_per_L
ounces_per_cubic_foot_oz_per_ft3
ounces_per_cubic_inch_oz_per_in3
ounces_per_gallon_oz_per_gal
pounds_per_cubic_foot_lb_per_ft3
pounds_per_cubic_inch_lb_per_in3
pounds_per_gallon_lb_per_gal
slugs_per_cubic_foot_slug_per_ft3

energy

UnitLiteralPrefixes
joules_Jyes
calories_calyes
kilowatt_hours_kWh
watt_hours_Wh
british_thermal_units_BTU
british_thermal_units_iso_BTU_iso
british_thermal_units_59_BTU59
therms_thm
foot_pounds_ftlbf
ergs_erg
calories_it_cal_it
tons_of_tnt_tTNT

energy density

UnitLiteralPrefixes
joules_per_meter_cubed_J_per_m3yes

force

UnitLiteralPrefixes
newtons_Nyes
pounds_lbf
dynes_dyn
kiloponds_kp
poundals_pdl
kips_kip
ounces_force_ozf
grams_force_gf
short_tons_force_tonf
long_tons_force_ltonf
sthenes_sn

frequency

UnitLiteralPrefixes
hertz_Hzyes

illuminance

UnitLiteralPrefixes
lux_lxyes
footcandles_fc
lumens_per_square_inch_lm_per_in2
phots_ph

impedance

UnitLiteralPrefixes
ohms_Ohmyes

inductance

UnitLiteralPrefixes
henries_Hyes

irradiance

UnitLiteralPrefixes
watts_per_meter_squared_W_per_m2yes

jerk

UnitLiteralPrefixes
meters_per_second_cubed_mps3yes
feet_per_second_cubed_fps3

length

UnitLiteralPrefixes
meters_myes
feet_ft
inches_in
mils_mil
miles_mi
nautical_miles_nmi
astronomical_units_au
lightyears_ly
parsecs_pc
angstroms_angstrom
cubits_cbt
fathoms_ftm
chains_ch
furlongs_fur
hands_hand
leagues_lea
nautical_leagues_nl
yards_yd
rods_rod
links_li
barleycorns_bc
nails_nail
spans_span
picas_pica
points_pnt

luminance

UnitLiteralPrefixes
candelas_per_square_meter_cd_per_m2yes
stilbs_sb
apostilbs_asb
brils_bril
skots_sk
lamberts_la
millilamberts_mla
foot_lamberts_ftL

luminous flux

UnitLiteralPrefixes
lumens_lmyes

luminous intensity

UnitLiteralPrefixes
candelas_cdyes

magnetic field strength

UnitLiteralPrefixes
teslas_Teyes
gauss_G

magnetic flux

UnitLiteralPrefixes
webers_Wbyes
maxwells_Mx

mass

UnitLiteralPrefixes
grams_gyes
tonnes_t
pounds_lb
long_tons_ln_conversion_factor
short_tons_sh_conversion_factor
stone_st
ounces_oz
carats_ct
slugs_slug
grains_gr
avoirdupois_drams_dr_av
pennyweights_dwt
troy_ounces_ozt
troy_pounds_lbt
hundredweights_cwt
short_hundredweights_sh_cwt

power

UnitLiteralPrefixes
watts_Wyes
horsepower_hp
metric_horsepower_hpM
electrical_horsepower_hpE
tons_of_refrigeration_TR

pressure

UnitLiteralPrefixes
pascals_Payes
bars_bar
millibars_mbar
atmospheres_atm
pounds_per_square_inch_psi
torrs_torr
millimeters_of_mercury_mmHg
inches_of_mercury_inHg
technical_atmospheres_at
pounds_per_square_foot_psf
kips_per_square_inch_ksi
baryes_Ba
piezes_pz
centimeters_of_water_cmH2O
millimeters_of_water_mmH2O
inches_of_water_inH2O

radiance

UnitLiteralPrefixes
watts_per_steradian_per_meter_squared_W_per_srm2yes

radiant intensity

UnitLiteralPrefixes
watts_per_steradian_W_per_sryes

radiation

UnitLiteralPrefixes
becquerels_Bqyes
grays_Gyyes
sieverts_Svyes
curies_Ci
rutherfords_rd
radiation_absorbed_dose_rads
roentgens_equivalent_man_rem

solid angle

UnitLiteralPrefixes
steradians_sryes
degrees_squared_deg2
spats_sp

spectral flux

UnitLiteralPrefixes
watts_per_meter_W_per_myes

spectral intensity

UnitLiteralPrefixes
watts_per_steradian_per_meter_W_per_srmyes

spectral irradiance

UnitLiteralPrefixes
watts_per_meter_cubed_W_per_m3yes

spectral radiance

UnitLiteralPrefixes
watts_per_steradian_per_meter_cubed_W_per_srm3yes

substance

UnitLiteralPrefixes
mols_molyes
pound_moles_lbmol

substance concentration

UnitLiteralPrefixes
molars_Myes

substance mass

UnitLiteralPrefixes
grams_per_mole_g_per_molyes

temperature

UnitLiteralPrefixes
kelvin_K
celsius_degC
fahrenheit_degF
reaumur_Re
rankine_Ra

time

UnitLiteralPrefixes
seconds_syes
minutes_min
hours_hr
days_d
weeks_wk
years_yr
julian_years_a_j
gregorian_years_a_g
fortnights_fn
decades_dec
centuries_cent
millennia_kyr

torque

UnitLiteralPrefixes
newton_meters_Nm
pound_feet_lbf_ft
foot_poundals_ftpdl
inch_pounds_inlb
meter_kilograms_mkgf

velocity

UnitLiteralPrefixes
meters_per_second_mps
feet_per_second_fps
miles_per_hour_mph
kilometers_per_hour_kph
knots_kts
feet_per_minute_fpm
meters_per_minute_mpm
inches_per_second_ips
kilometers_per_second_kmps

viscosity

UnitLiteralPrefixes
pascal_seconds_Pa_s
poise_P
centipoise_cP
square_meters_per_second_m2_per_s
stokes_St
centistokes_cSt

voltage

UnitLiteralPrefixes
volts_Vyes
statvolts_statV
abvolts_abV

volume

UnitLiteralPrefixes
cubic_meters_m3
cubic_millimeters_mm3
cubic_kilometers_km3
liters_Lyes
cubic_inches_in3
cubic_feet_ft3
cubic_yards_yd3
cubic_miles_mi3
gallons_gal
quarts_qt
pints_pt
cups_c
fluid_ounces_fl_oz
barrels_bl
bushels_bu
cords_cord
cubic_fathoms_fm3
tablespoons_tbsp
teaspoons_tsp
pinches_pinch
dashes_dash
drops_drop
fifths_fifth
drams_dr
gills_gi
pecks_pk
sacks_sck
shots_shts
strikes_strk

volume flow rate

UnitLiteralPrefixes
cubic_meters_per_second_m3_per_s
cubic_meters_per_hour_m3_per_hr
liters_per_second_L_per_s
liters_per_minute_L_per_min
gallons_per_minute_gpm
gallons_per_hour_gph
cubic_feet_per_second_cfs
cubic_feet_per_minute_cfm

Physical constants

Provided in units::constants as typed quantities (each carries its dimension, so it participates in dimensional analysis). Values are the 2018 CODATA recommended values.

SymbolConstantValue
piRatio of a circle's circumference to its diameter1
cSpeed of light in vacuum299792458.0
GNewtonian constant of gravitation6.67430e-11
hPlanck constant6.62607015e-34
h_barReduced Planck constant1.054571817e-34
mu0vacuum permeability1.25663706212e-6
epsilon0vacuum permittivity8.8541878128e-12
Z0characteristic impedance of vacuum376.730313668
k_eCoulomb's constant8.9875517923e9
eelementary charge1.602176634e-19
m_eelectron mass9.1093837015e-31
m_pproton mass1.67262192369e-27
mu_BBohr Magneton9.2740100783e-24
N_AAvogadro's Number6.02214076e23
RGas constant8.314462618
k_BBoltzmann constant1.380649e-23
FFaraday constant96485.33212
sigmaStefan-Boltzmann constant5.670374419e-8

More capabilities

Beyond the catalog: unit-aware <cmath> (found by ADL), std::chrono::duration interop, std::hash and std::numeric_limits specializations, NaN/infinity support, self-describing binary serialization, optional nlohmann/json serialization, a concept vocabulary (UnitType, ConversionFactorType, …) plus a per-dimension concept for every dimension (Velocity, Force, Length, …) so you can constrain a template on a physical quantity by dimension (void f(Velocity auto v)), non-linear (decibel) scales, and affine temperature. Each has a how-to or reference page under docs/.


Documentation

The manual is under docs/ (hub: docs/README.md). The generated API reference is published at https://nholthaus.github.io/units/.

Learn

Explain

How-to

Reference

Meta


Citing

If you use units in academic or published work, a citation is appreciated. The repository includes a CITATION.cff, so GitHub's "Cite this repository" button (top right of the repository page) generates a formatted citation and BibTeX for you. A BibTeX entry:

@software{holthaus_units,
  author  = {Holthaus, Nic},
  title   = {units: a compile-time C++ dimensional-analysis and unit-conversion library},
  url     = {https://github.com/nholthaus/units},
  license = {MIT}
}

License

units is distributed under the MIT License. Copyright © Nic Holthaus.

compile-time
cpp14
dimensional-analysis
header-only
no-dependencies
template-metaprogramming
unit-conversion

Project health

Overall

4.2/10

Maintained

10/10

Code review

0/10
OpenSSF Scorecard

Significant stargazers

gnzlbg

365 followers · starred Oct 2016

Mark Daoust

552 followers · starred Jan 2020

Dmitry Atamanov

197 followers · starred Jan 2026

Mario Lang

80 followers · starred Jul 2018