TU is a C++ header-only library for typesafe unit operations. With TU you create instances of templated structs that represent units and operate on these instead of operating on numbers.
Unit<prefix::milli, second> s(5.0f);
Unit<prefix::micro, ampere> a(10.0f);
Unit<prefix::no_prefix, coulomb> c = s * a;
std::cout << c.value << std::endl; // prints 5e-08
If you need a non-SI unit you define it by declaring a simple struct. This is how you would define the unit
degree_Fahrenheit
:
struct degree_Fahrenheit : Non_coherent_unit<1.0f / 1.8f, -32.0f, degree_Celsius> {
using Non_coherent_unit<1.0f / 1.8f, -32.0f, degree_Celsius>::Base;
};
You can use the new unit like any other unit already defined in TU:
Unit<prefix::no_prefix, degree_Celsius> c(0.0f);
Unit<prefix::no_prefix, degree_Fahrenheit> f(c);
std::cout << f.value << std::endl; // prints 32
TU handles prefixes under the hood so you have complete freedom mixing prefixes.
Unit<prefix::milli, second> s1(20.0f);
Unit<prefix::micro, second> s2(30.0f);
Unit<prefix::nano, second> s3 = s1 + s2;
std::cout << s3.value << std::endl; // prints 2.003e+07
Attempts to initialize or operate on incompatible units will result in compilation failure.
Unit<prefix::milli, second> s(20.0f);
Unit<prefix::micro, ampere> a(10.0f);
auto sa = s + a; // compilation failure
Unit<prefix::micro, ampere> a2 = s * a; // compilation failure
Current supported typesafe operations on units are:
- Addition (+)
- Subtraction (-)
- Multiplication (*)
- Division (/)
- Power to arbitrary floating point number (pow)
- Square root (sqrt)
- Comparison <, >, <=, >=, !=, ==.
- Unary operations on scalar units (e.g trigonometric function like
std::sin
), - Unit conversion (e.g. mK (milli Kelvin) to °F (degrees Fahrenheit))
By default TU uses single precision (float
) as the underlying data type. To use double precision (double
), assign double
to the macro TU_TYPE
i.e include #define TU_TYPE double
before the inclusion of typesafe_units.h
. If you use CMake, the definition can be made by
target_compile_definitions(my_target PRIVATE TU_TYPE=double)
TU requires a c++20 compliant compiler. Specifically TU utilizes float non-type template arguments.
For the test suite that comes with TU to work, your system needs to have support for ANSI escape sequences since the output uses colours. This should work on fairly recent Windows 10 system, linux and macOS. It might be a problem on Windows 7 though. If you find that this is a showstopper for you please let us know. If enough people run TU on systems that does not have support for ANSI escape sequences, we will remove it.
TU is continuously built on Windows and Linux (Ubuntu) with MSVC and GCC respectively. For exact versions of tested compilers, please see the build logs of the github ci builds.
TU is a header-only library. To use TU in you project, simply include the header typesafe_units/include/tu/typesafe_units.h
.
If you want to use TU as a CMake package you can use the CMake command find_package
as follows and include the header by #include "tu/typesafe_units.h"
#
# The package is called TU. Include it with `find_package`.
# If CMake does not find the package you can specify the path to the TU root as
# a HINT. You are required to state the exact version of TU that you want to
# use. <version> should be given on the format major.minor.patch e.g. 1.2.3.
#
find_package(TU <version> REQUIRED HINTS "<absolute path to TU root>")
#
# The library itself is called `tu` (lowercase). Link your target to it.
#
target_link_libraries(my_target tu)
#
# Make some configurations.
# TU_TYPE sets the underlying datatype of TU. Use float or double.
#
set_property(TARGET my_target PROPERTY CXX_STANDARD 20)
target_compile_definitions(my_target PRIVATE TU_TYPE=<float, double>)
TU comes with its own test suite. It does not rely on any external testing tool. To verify that TU runs on your system, build the test suite with CMake.
The following instruction assumes that you do an out of source build in a directory under the repository root.
cmake .. -G <generator>
cmake --build . --config <build type>
Run the test suite
ctest -V
The test suite test TU for both float and double as underlying datatype.
The aim of TU is to be
- compliant to definitions and guides of official bodies. For SI units, TU aims for compliance with the definitions issued by Bureau International des Poids et Mesures (BIPM). See link to bimp.org for details.
- (type)safe
- easy to use
- light weight
TU is released under the MIT license. https://mit-license.org/
The intrinsic data type used by TU is defined in the preprocessor macro TU_TYPE
.
TU_TYPE
can be float
or double
. All values and floating point template argumets will have the type defined by TU_TYPE
.
The main namespace of TU is tu
.
Functionality inside tu
that is located in the namespace internal
is not public and should only be used implicitly by public classes and methods.
These are the base units with floating point template arguments that determins the power of the base unit.
The base units are used to build Coherent_unit
s
The definition of each base unit looks as follows where the unit is denoted X
.
template<TU_TYPE p>
struct X : internal::Base_unit<p>{};
The base unit per_second
can be declared through
s<(TU_TYPE)-1.0f>;
The Coherent_unit
struct represents a unit that is a multiple of all base units: s, m, kg, A, K, mol and cd.
A specific coherent unit should be defined by inheriting from a Coherent_unit
The specific coherent unit newton
is defined as
struct newton: Coherent_unit<s<(TU_TYPE)-2.0>, m<(TU_TYPE)1.0>, kg<(TU_TYPE)1.0>, A<(TU_TYPE)0.0>, K<(TU_TYPE)0.0>, mol<(TU_TYPE)0.0>, cd<(TU_TYPE)0.0>>{};
i.e. it has the unit kg m / s^2
Note that computations using seconds should use the Coherent_unit
second
and not the base unit s
.
second
is defined as
struct second: Coherent_unit<s<(TU_TYPE)1.0>, m<(TU_TYPE)0.0>, kg<(TU_TYPE)0.0>, A<(TU_TYPE)0.0>, K<(TU_TYPE)0.0>, mol<(TU_TYPE)0.0>, cd<(TU_TYPE)0.0>>{};
All base units are defined as Coherent_unit
s in similar fashion.
A Non_coherent_unit
is a unit that is scaled or shifted relative to a base unit. The value of the Non_coherent_unit
is related to the value of the base unit through v = a * b + c
where v
is the value of the Non_coherent_unit
, b
is the value of the base unit. a
and c
are the scaling and shift respectively.
Example of Non_coherent_unit
s are minute
, hour
and degree_Celcius
.
A Non_coherent_unit
is a templated struct that has the scaling factor, the shift and the base unit as template parameters.
minute
and hour
are defined by
struct minute : Non_coherent_unit<(TU_TYPE)60.0, (TU_TYPE)0.0, second> {
using Non_coherent_unit<(TU_TYPE)60.0, (TU_TYPE)0.0, second>::Base;
};
struct hour : Non_coherent_unit<(TU_TYPE)60.0, (TU_TYPE)0.0, minute> {
using Non_coherent_unit<(TU_TYPE)60.0, (TU_TYPE)0.0, minute>::Base;
};
degree_Celsius
is defined by
struct degree_Celsius : Non_coherent_unit<(TU_TYPE)1.0, (TU_TYPE)273.15, kelvin> {
using Non_coherent_unit<(TU_TYPE)1.0, (TU_TYPE)273.15, kelvin>::Base;
};
The using
statement is of internal concern only. If new Non_coherent_unit
s are created, just follow the pattern.
The Unit
is the intended public unit type.
It is a templated struct with a prefix and a unit as template parameters.
A unit variable, u
, is declared by
Unit<prefix, unit> u;
where prefix is one of the prefix types defined in the enum struct prefix
and unit is a Coherent_unit
or a Non_coherent_unit
.
A Unit
can be constructed from a value of type TU_TYPE
or from another unit of the same type of unit.
A unit representing 5 nano seconds is created by
Unit<prefix::milli, second> ms(5);
This can in turn be used to create a new unit variable.
Unit<prefix::no_prefix, minute> mi(ms)
The values of ms
and mi
are obtained through the value
member.
std::cout << ms.value << " " << mi.value << std::endl; // prints 5.0 8.3333e-5
The following prefixes are defined and can be used when creating Unit
s.
- yocto = 10-24
- zepto = 10-21
- atto = 10-18
- femto = 10-15
- pico = 10-12
- nano = 10-9
- micro = 10-6
- milli = 10-3
- centi = 10-2
- deci = 10-1
- no_prefix = 100
- deca = 101
- hecto = 102
- kilo = 103
- mega = 106
- giga = 109
- terra = 1012
- peta = 1015
- exa = 1018
- zetta = 1021
- yotta = 1024
The convert_to
function converts one unit variable to a different unit (of same basic type)
It is defined by
template<prefix to_prefix,
typename To_unit,
prefix from_prefix,
typename From_unit,
template<prefix, typename> typename Unit>
requires std::is_same<typename From_unit::Base, typename To_unit::Base>::value
Unit<to_prefix, To_unit> convert_to(const Unit<from_prefix, From_unit>& from) noexcept
An example of usage could be
Unit<prefix::no_prefix, Minute> m(1.0f);
std::cout << tu::convert_to<prefix::milli,Second>(m).value << std::endl; // prints 60000.0
TU supports the binary operators +
and -
(addition and subtraction) on units. Conversions are handled under the hood of TU.
Unit<prefix::no_prefix, minute> mi(5.0f);
Unit<prefix::no_prefix, hour> h(1.0f);
Unit<prefix::milli, second> ms = h + mi;
std::cout << ms.value << std::endl; // prints 3.9e6
Note that the result of the +
and -
operators on units is not a Unit
but a Coherent_unit
. If we instead would do
Unit<prefix::no_prefix, minute> mi(5.0f);
Unit<prefix::no_prefix, hour> h(1.0f);
auto cu = h + mi;
std::cout << cu.base_value << std::endl; // prints 3900.0
This is because TU does not know what Unit
to construct from the operation. TU falls back on the fundamental Coherent_unit
s and cu
will be of type
Coherent_unit<s<(TU_TYPE)1.0>, m<(TU_TYPE)0.0>, kg<(TU_TYPE)0.0>, A<(TU_TYPE)0.0>, K<(TU_TYPE)0.0>, mol<(TU_TYPE)0.0>, cd<(TU_TYPE)0.0>>;
Note also that Coherent_unit
does not have a value
member but only a base_value
If we would like a specific Unit
representation of the operation, we have to explicitly state the Unit
as in the first example and the result of the operation will be used to construct the desired Unit
.
Applying the +
and -
operators on Unit
s that don't have the same underlying Coherent_unit
will result in compilation failure e.g. it is not possible to add to variables of type newton
and second
.
TU supports the binary operators *
and /
(multiplication and division).
Unit<prefix::milli, second> s(5.0f);
Unit<prefix::micro, ampere> a(10.0f);
Unit<prefix::micro, coulomb> c = s * a;
std::cout << c.value << std::endl; // prints 5e-02
Note that the result of the *
and /
operators on units is not a Unit
but a Coherent_unit
. If we instead would do
Unit<prefix::milli, second> s(5.0f);
Unit<prefix::micro, ampere> a(10.0f);
auto cu = s * a;
std::cout << cu.base_value << std::endl; // prints 5e-08
This is because TU does not know what Unit
to construct from the operation. TU falls back on the fundamental Coherent_units
and cu
will be of type
Coherent_unit<s<(TU_TYPE)1.0>, m<(TU_TYPE)0.0>, kg<(TU_TYPE)0.0>, A<(TU_TYPE)1.0>, K<(TU_TYPE)0.0>, mol<(TU_TYPE)0.0>, cd<(TU_TYPE)0.0>>;
Note also that Coherent_unit
does not have a value
member but only a base_value
If we would like a specific Unit
representation of the operation, we have to explicitly state the Unit
as in the first example and the result of the operation will be used to construct the desired Unit
.
Note that trying to create a Unit that does not have the correct Coherent_unit
base would result in compilation failure.
TU implements comparison operators for units with the same underlying Coherent_unit
.
Comparison is made to the Units base_value
s so that
Unit<prefix::milli, metre> me1(5.0f);
Unit<prefix::no_prefix, metre> me2(0.004f);
if (me2 < me1) std::cout << "true as expected"; // prints "true as expected"
TU implements a pow
operator for units.
Unit<prefix::milli, metre> me(5.0f);
auto ch = pow<2.0f>(me);
std::cout << ch.base_value << std::endl; // prints 2.5 * 10^-5
ch
will be of type
Coherent_unit<s<(TU_TYPE)0.0>, m<(TU_TYPE)2.0>, kg<(TU_TYPE)0.0>, A<(TU_TYPE)0.0>, K<(TU_TYPE)0.0>, mol<(TU_TYPE)0.0>, cd<(TU_TYPE)0.0>>;
To construct a Unit
directly we could do
Unit<prefix::milli, metre> me(5.0f);
Unit<prefix::milli, metre_squared> m2 = pow<2.0f>(me);
std::cout << m2.value << std::endl; // prints 2.5 * 10^-2
Note that Unit<prefix::milli, metre_squared>
means 10-3m2
and not (mm)2
To the unit (mm)2 is equivalent to Unit<prefix::micro, metre_squared>
Note that the power is not restricted to integers.
The operation
sqrt(unit).
is equivalent to
pow<0.5>(unit).
Not that since TU uses floating point powers, it is not guaranteed that applying first pow<2.0>
and the sqrt
on a unit would yield the exact unit back.
TU supports unary operations on scalar units i.e. units where all basic unit powers are 0
. Examples of scalar units is radian
and degree
.
unop
is a template function that applies any unary function that takes a TU_TYPE
and returns a TU_TYPE to the underlying value of the unit if it is a scalar unit. The function returns a scalar Coherent_unit initialized with the value of the performed operation. This makes it possible to operate with any unary function (subjected to the restrictions above) from the standard library on a Unit or Coherent_unit. unop can take both unary functions and lambda expressions as template parameter.
Unit<prefix::no_prefix, degree> angle(90);
std::cout << unop<std::sin>(angle).base_value; // prints 1
Note that unop
operates on the base_value
on a unit. In the case of degree
the base unit is radian
(90 degrees == pi/2 radians) and the std::sin
function yields the correct result.
- second
- metre
- kilogram
- ampere
- kelvin
- mole
- candela
- hertz
- becquerel
- ohm
- siemens
- farad
- lumen
- weber
- gray
- sievert
- watt
- newton
- lux
- radian
- joule
- steradian
- katal
- pascal
- coulomb
- henry
- tesla
- volt
- metre_per_second
- second_squared
- metre_cubed
- metre_squared
- minute
- hour
- day
- degree_Celsius
- gram
- tonne
- dalton
- unified_atomic_mass_unit
- electronvolt
- litre
- degree
- arc_minute
- arc_second
- hectare
- astronomical_unit