Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Unit Template Library (UTL) is an easy to use, header-only C++17 library for type-safe SI units.

Every quantity is represented by its seven SI base dimension exponents (time, length, mass, electric current, thermodynamic temperature, amount of substance, luminous intensity) plus an eighth angle pseudo-dimension (rad = angle¹, sr = angle²), all encoded as template parameters. The compiler tracks dimensions through arithmetic, so adding a Length to a Time is a compile-time error, while dividing a Length by a Time yields a Velocity.

The library is the successor of the Robotic Template Library.

Design goals

  • Type safety — dimension errors are caught at compile time.
  • Zero overhead — a unit is exactly one float or double at runtime.
  • Header-only — nothing to build or link; include and go.

License

MIT License, Copyright (c) 2024 Adam Ligocki.

Feedback

For technical feedback, bug reports, or feature proposals please use the GitHub issue tracker.

Getting Started

UTL requires a C++17 compiler. There are no runtime dependencies; the tests use GoogleTest.

CMake with find_package

Install the library once:

git clone https://github.com/adamek727/Unit-Template-Library
cd Unit-Template-Library
cmake -B build
cmake --install build

Then in your project:

find_package(utl REQUIRED)
target_link_libraries(your_target utl::utl)

CMake with add_subdirectory

Add the repository as a git submodule:

git submodule add https://github.com/adamek727/Unit-Template-Library libs/utl

And in your CMakeLists.txt:

add_subdirectory(libs/utl)
target_link_libraries(your_target utl::utl)

CMake with FetchContent

include(FetchContent)
FetchContent_Declare(utl
        GIT_REPOSITORY https://github.com/adamek727/Unit-Template-Library
        GIT_TAG v3.1.0)
FetchContent_MakeAvailable(utl)
target_link_libraries(your_target utl::utl)

Conan

A header-only recipe ships in conanfile.py. Until the package is on Conan Center, export it locally with conan create ., then depend on unit-template-library/3.1.0 and link utl::utl.

vcpkg

An overlay port lives in packaging/vcpkg/ports:

vcpkg install unit-template-library --overlay-ports=packaging/vcpkg/ports

In every case the imported CMake target is utl::utl.

Single header

For a zero-setup drop-in, copy single_include/utl/utl.hpp (an amalgamation of the whole library generated by tools/amalgamate.py) into your project and include it directly. The opt-in utl/io.hpp is not part of the amalgamation.

Include the headers

#include <utl/utl.hpp>

Building the examples and tests

git clone https://github.com/adamek727/Unit-Template-Library
cd Unit-Template-Library
cmake -B build -DENABLE_TESTS=1 -DENABLE_EXAMPLES=1
cmake --build build -j
ctest --test-dir build
./build/examples/usage_example

Usage

Construction and conversion

Units are constructed from their base SI value and converted through named accessors:

auto length = utl::Length<float>(1500);
std::cout << length.km() << std::endl;

auto area = utl::Area<float>(6543);
std::cout << area.m2() << std::endl;

Arithmetic with predefined operators

Common physical relations are available directly:

auto v = utl::Velocity<float>(30);
auto t = utl::Time<float>(5);
auto d = v * t;
std::cout << "Distance: " << d.m() << " m" << std::endl;

auto g = utl::Acceleration<float>(9.81);
auto tt = utl::Time<float>(10);
auto s = 0.5 * g * tt * tt;
std::cout << "Free fall: " << s.m() << " m" << std::endl;

Arbitrary dimensional expressions

Any dimensionally valid expression maps back to its named unit automatically. Combinations without a named SI unit yield the raw BaseUnit, which still tracks dimensions and exposes value():

auto m = utl::Mass<float>(1);
auto &c = utl::speed_of_light_f;
auto e = m * c * c;
std::cout << "Energy: " << e.J() << " J" << std::endl;

Unit literals

The utl::literals namespace provides double-precision literal suffixes for common units:

using namespace utl::literals;

auto distance = 120.0_km + 500.0_m;
auto speed = distance / 2.0_h;
if (speed > 50.0_kmph) {
    std::cout << "Speeding: " << speed.kmph() << " km/h" << std::endl;
}

Available suffixes: _s, _ms, _min, _h, _m, _km, _mm, _kg, _g, _A, _K, _mol, _cd, _mps, _kmph, _N, _J, _W, _Hz, _Pa, _V, _rad.

Comparisons and compound assignment

Units of the same dimension support ==, !=, <, <=, >, >= as well as +=, -=, scalar *=, /= and unary minus.

Stream output

Including utl/io.hpp enables printing any unit as its value followed by its SI dimensions:

#include <utl/io.hpp>

std::cout << utl::Velocity<float>(30) << std::endl;  // 30 [s^-1 m]

Precision selection

Every unit is templated on its storage type:

auto unit_f = utl::Unit<float>(utl::PI);
auto unit_d = utl::Unit<double>(utl::PI);

Mixing storage types in the same expression is allowed; the result is promoted to the wider type (std::common_type_t):

auto sum = utl::Length<float>(1.0f) + utl::Length<double>(2.0);  // Length<double>
auto d   = utl::Velocity<float>(30.0f) * utl::Time<double>(5.0); // Length<double>

Physical constants

Predefined constants are available in float (_f) and double (_d) variants:

ConstantUnit
speed_of_light_f / _dVelocity
cesium_hyperfine_freq_f / _dFrequency
elementary_charge_f / _dElectricCharge
planc_constant_f / _dEnergy × Time
boltzmann_constant_f / _dEnergy / Temperature
avogadro_constant_f / _d1 / AmountOfSubstance
luminous_efficacy_f / _dLuminousFlux / Power

Temperature differences

Temperature is affine: °C and °F have offset origins, so the difference of two temperatures is a displacement, not an absolute point. Temperature - Temperature therefore yields a TemperatureDelta, whose degC() / degF() apply only the scale factor (never the +273.15 / +32 offset). Adding two absolute temperatures is deleted; add a delta instead:

using Temp = utl::ThermodynamicTemperature<double>;

auto d = Temp(20, Temp::TYPE::CELSIUS) - Temp(5, Temp::TYPE::CELSIUS); // TemperatureDelta
d.degC();                                                             // 15 (not -258.15)

auto warmer = Temp(20, Temp::TYPE::CELSIUS) + utl::TemperatureDelta<double>(5); // 25 °C

Angles, solid angles and torque

Angle is tracked as an eighth pseudo-dimension, so rad, sr and a plain scalar are distinct types:

auto solid = utl::Angle<double>(2.0) * utl::Angle<double>(3.0); // SolidAngle, 6 sr
auto back  = utl::sqrt(utl::SolidAngle<double>(9.0));           // Angle, 3 rad

// luminous intensity * solid angle = luminous flux (cd * sr = lm)
auto flux = utl::LuminousIntensity<double>(60.0) * utl::SolidAngle<double>(2.0); // 120 lm

// torque (N*m = energy / angle) is a distinct type from energy
auto torque = utl::Energy<double>(10.0) / utl::Angle<double>(2.0); // Torque, 5 N*m
auto work   = torque * utl::Angle<double>(2.0);                    // Energy, 10 J

// trigonometry takes an Angle and returns a scalar
auto y = utl::sin(utl::Angle<double>(90.0, utl::Angle<double>::TYPE::DEG)); // 1.0

The angle exponent defaults to zero, so every other named unit and all existing code are unaffected.

Precise time (utl/time_point.hpp)

For real-time use where sub-microsecond precision matters, the opt-in utl/time_point.hpp provides TimeDuration and TimeStamp — integer-native (int64 nanosecond) wrappers with affine semantics. They never use floating point, so they do not lose precision the way double seconds do near a large epoch.

#include <utl/time_point.hpp>
using namespace utl;

auto dt = TimeStamp(1500000000) - TimeStamp(1000000000); // TimeDuration, 500 ms
auto later = TimeStamp(1000000000) + milliseconds(250);  // TimeStamp
// TimeStamp + TimeStamp does not compile (adding absolute times is meaningless)

auto seconds_as_double = milliseconds(1500).to_time();   // Time<double>, 1.5 s

Construct durations with nanoseconds / microseconds / milliseconds / seconds, read them with ns() / us() / ms() / s() (integer counts), and cross into the dimensional world with to_time().

utl/chrono.hpp bridges these types to std::chrono:

#include <utl/chrono.hpp>

auto d  = to_duration(std::chrono::milliseconds(250));   // TimeDuration
auto c  = to_chrono(milliseconds(250));                  // std::chrono::nanoseconds
auto ts = to_timestamp(std::chrono::steady_clock::now()); // TimeStamp
auto tp = to_chrono<std::chrono::steady_clock>(ts);       // std::chrono::time_point

Units Reference

All units are templates over the storage type, e.g. Length<float> or Length<double>. Shorthand aliases with _f and _d suffixes exist for every unit, e.g. Length_f, Length_d.

Base units

UnitSI unitExample accessors
Timesecond [s]s(), ms(), us(), ns(), min(), h(), day()
Lengthmeter [m]m(), km(), mm(), um(), nm(), mi(), yd(), ft(), in()
Masskilogram [kg]kg(), g(), mg(), ug(), tonne(), lb(), oz()
ElectricCurrentampere [A]A()
ThermodynamicTemperaturekelvin [K]K(), degC(), degF()
TemperatureDeltakelvin [K] (a difference)K(), degC(), degF() (offset-free)
AmountOfSubstancemole [mol]mol()
LuminousIntensitycandela [cd]cd()

Derived units

UnitSI unit
AbsorbedDosegray [Gy]
Acceleration[m/s²]
Activitybecquerel [Bq]
Angleradian [rad]
Area[m²]
Capacitancefarad [F]
CatalyticActivitykatal [kat]
Concentration[mol/m³]
Conductancesiemens [S]
CurrentDensity[A/m²]
Density[kg/m³]
DoseEquivalentsievert [Sv]
ElectricChargecoulomb [C]
Energyjoule [J]
Forcenewton [N]
Frequencyhertz [Hz]
Illuminancelux [lx]
Inductancehenry [H]
Luminance[cd/m²]
LuminousFluxlumen [lm]
MagneticFluxweber [Wb]
MagneticFluxDensitytesla [T]
Powerwatt [W]
Pressurepascal [Pa]
SolidAnglesteradian [sr]
Torquenewton-metre [N·m] (energy / angle)
Velocity[m/s]
Vergence[1/m] (diopter)
Voltagevolt [V]
Volume[m³]

Units sharing a dimension signature

Several SI units share the same dimension exponents, so automatic result-type mapping has to pick one winner; construct the other explicitly when needed:

DimensionsMapped resultConstruct explicitly
1/sFrequencyActivity
m²/s²AbsorbedDoseDoseEquivalent
KThermodynamicTemperatureTemperatureDelta

The angle pseudo-dimension

An eighth exponent tracks angle (rad = angle¹, sr = angle²), so Angle, SolidAngle and a dimensionless scalar are now distinct, and LuminousFlux (cd·sr) is distinct from LuminousIntensity (cd) — both get their own mapped result type instead of colliding. The exponent defaults to zero, so every other unit and all existing code are unchanged.

This is also what makes Torque (N·m = energy / angle) a type distinct from Energy (J): Energy / Angle == Torque and Torque * Angle == Energy, while a plain Force * Length still maps to Energy.