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:
| Constant | Unit |
|---|---|
speed_of_light_f / _d | Velocity |
cesium_hyperfine_freq_f / _d | Frequency |
elementary_charge_f / _d | ElectricCharge |
planc_constant_f / _d | Energy × Time |
boltzmann_constant_f / _d | Energy / Temperature |
avogadro_constant_f / _d | 1 / AmountOfSubstance |
luminous_efficacy_f / _d | LuminousFlux / 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
| Unit | SI unit | Example accessors |
|---|---|---|
Time | second [s] | s(), ms(), us(), ns(), min(), h(), day() |
Length | meter [m] | m(), km(), mm(), um(), nm(), mi(), yd(), ft(), in() |
Mass | kilogram [kg] | kg(), g(), mg(), ug(), tonne(), lb(), oz() |
ElectricCurrent | ampere [A] | A() |
ThermodynamicTemperature | kelvin [K] | K(), degC(), degF() |
TemperatureDelta | kelvin [K] (a difference) | K(), degC(), degF() (offset-free) |
AmountOfSubstance | mole [mol] | mol() |
LuminousIntensity | candela [cd] | cd() |
Derived units
| Unit | SI unit |
|---|---|
AbsorbedDose | gray [Gy] |
Acceleration | [m/s²] |
Activity | becquerel [Bq] |
Angle | radian [rad] |
Area | [m²] |
Capacitance | farad [F] |
CatalyticActivity | katal [kat] |
Concentration | [mol/m³] |
Conductance | siemens [S] |
CurrentDensity | [A/m²] |
Density | [kg/m³] |
DoseEquivalent | sievert [Sv] |
ElectricCharge | coulomb [C] |
Energy | joule [J] |
Force | newton [N] |
Frequency | hertz [Hz] |
Illuminance | lux [lx] |
Inductance | henry [H] |
Luminance | [cd/m²] |
LuminousFlux | lumen [lm] |
MagneticFlux | weber [Wb] |
MagneticFluxDensity | tesla [T] |
Power | watt [W] |
Pressure | pascal [Pa] |
SolidAngle | steradian [sr] |
Torque | newton-metre [N·m] (energy / angle) |
Velocity | [m/s] |
Vergence | [1/m] (diopter) |
Voltage | volt [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:
| Dimensions | Mapped result | Construct explicitly |
|---|---|---|
| 1/s | Frequency | Activity |
| m²/s² | AbsorbedDose | DoseEquivalent |
| K | ThermodynamicTemperature | TemperatureDelta |
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.