This project aims to provide a type-safe, efficient, and easy-to-use C++11 library for representing real-world units. It is similar to a more abstract version of std::duration<>
available in the C++ <chrono>
header.
The requirements of this project are:
- No common or preferred system of measure (e.g. storing everything as metric and converting as needed)
- Respect the user's types
- No common, intermediate storage type (e.g. storing all numbers as
double
) - All operations on units should preserve value types as if just the value types were used
- No common, intermediate storage type (e.g. storing all numbers as
- Zero-overhead abstraction
- Do nothing at runtime that can't be done at compile-time
- All unit conversions must result in the same value that would result from hand-coding a multiplication by a one-way conversion factor
Any real-world unit of measure that has a concrete definition or unambiguous meaning is welcome in this library (e.g. pennyweight or bytes, but not cubits). If one you know of is missing, please create an issue or make a pull request. In case your project needs a unit that wouldn't be appropriate for inclusion here, see Creating Custom Units.
Currently, a few linear, angular, and temporal units are provided in their own headers; a full set of standard units are planned by v1.0. The available units are:
- Angular —
units/angular.hpp
radians
degrees
arcminutes
arcseconds
revolutions
rpm
- Linear —
units/linear.hpp
inches
feet
yards
miles
meters
fathoms
nautical_miles
(Note: prefixed versions follow the patternkilo_nautical_miles
,dozen_nautical_miles
, etc.)knots
- Temporal —
units/temporal.hpp
— these are also implicitly convertible to & fromstd::chrono::duration<>
sseconds
minutes
hours
hertz
- Digital —
units/digital.hpp
All unit types have scaled versions for the following prefixes:
- All the SI prefixes
- Everything from
atto*
toexa*
zepto*
/zetta*
andyocto*
/yotta*
if the platformstd::intmax_t
can represent them
- Everything from
- All the ISO/IEC 80000 binary prefixes
- Everything from
kibi*
toexbi*
zebi*
andyobi*
, again only if the platformstd::intmax_t
can represent them
- Everything from
dozen_*
bi*
semi*
While primarily header-only, units
uses modern (v3.0+) CMake to build & run its unit tests, as well as export configuration for an INTERFACE
-only library. While CMake will attempt installation at the system level by default, that is not recommended as it may interfere with the system's package manager(s). To install and use units
from an arbitrary directory, run something similar to following:
git clone https://github.com/JadeMatrix/units.git $UNIT_SOURCE
cd $UNITS_BUILD
cmake -D CMAKE_INSTALL_PREFIX=$UNITS_INSTALL $UNIT_SOURCE
make all test install
… where $UNIT_SOURCE
, $UNITS_BUILD
, and $UNITS_INSTALL
are the places where units
is cloned, built, and installed to, respectively. While these can all be the same location, this is not recommended as build and install outputs may interfere with (overwrite) the cloned source code.
Then, to use units
in your own CMake project, add the lib/cmake
under your units
install location to your project's CMAKE_PREFIX_PATH
variable:
cd /your/build/dir/
cmake -D CMAKE_PREFIX_PATH=$UNITS_INSTALL/lib/cmake/ /your/project/source/
Then, in your project's CMakeLists.txt
:
FIND_PACKAGE( JadeMatrix-units 0.2 REQUIRED COMPONENTS units )
There will now be an imported target by the name JadeMatrix::units::units
which you can pass to TARGET_LINK_LIBRARIES()
.
units
follows the <author>::<library>::
namespacing convention, which is intended to be aliased to simply units::
in most cases but permits disambiguation if required.
#include <units/angular.hpp>
#include <cmath> // For std::sin()
namespace units = ::JadeMatrix::units;
// "Base" type-safe sine function taking radians as standard
template< typename T >
constexpr units::ratio< T > sin( const units::radians< T >& r )
{
using std::sin;
return sin( static_cast< T >( r ) );
}
// Wrapper sine function taking degrees
template< typename T >
constexpr units::ratio< T > sin( const units::degrees< T >& r )
{
return sin( units::radians< T >{ r } );
}
units
also has an optional headers providing stringification functionality, all of which use argument-dependent lookup (ADL) for value types:
Stringify method | Header |
---|---|
to_string() |
units/stringify/to_string.hpp |
Stream format operators (<< ) |
units/stringify/ostream.hpp |
#include <units/linear.hpp>
#include <units/stringify/ostream.hpp>
#include <iostream>
namespace units = ::JadeMatrix::units;
void print_kiloyards_in_feet( units::kiloyards< int > kyd )
{
std::cout
<< kyd
<< " = "
<< units::feet< double >{ kyd }
<< std::endl
;
}
Creating custom units is generally very straightforward, essentially consisting of a traits/tag type and a number of relationships representing unit conversions. Some utility macros are also provided to define prefixed versions and add stringification support to new units. For example, the units inches
and feet
are defined basically as:
#include <units/core/constants.hpp>
// This header contains includes of all the necessary utilities for defining
// custom units (except constants)
#include <units/core/define_unit.hpp>
namespace custom
{
struct inch_traits {};
struct foot_traits {};
// Conversion relationship /////////////////////////////////////////////////
struct inches_feet_linear_relation
{
template< typename T > struct values
{
// Default `slope_num` of 1 used if not defined
static constexpr auto slope_num =
units::constants::foot_inches< T >::value;
// Default `slope_den` of 1 used if not defined
// Default `intercept` of 0 used if not defined
};
};
inches_feet_linear_relation units_linear_relation_lookup(
inch_traits&&,
foot_traits&&
); // No definition for this function; used only for ADL logic
// Prefixes definition /////////////////////////////////////////////////////
#define DEFINE_PREFIX_FOR_inches( PREFIX, SCALE ) \
template< typename T > using PREFIX##inches = units::unit< \
inch_traits, \
SCALE, \
T \
>;
#define DEFINE_PREFIX_FOR_feet( PREFIX, SCALE ) \
template< typename T > using PREFIX##feet = units::unit< \
foot_traits, \
SCALE, \
T \
>;
JM_UNITS_FOREACH_SCALE( DEFINE_PREFIX_FOR_inches )
JM_UNITS_FOREACH_SCALE( DEFINE_PREFIX_FOR_feet )
#undef DEFINE_PREFIX_FOR_inches
#undef DEFINE_PREFIX_FOR_feet
// Stringification support for all prefixes ////////////////////////////////
JM_UNITS_DEFINE_STRINGS_FOR_TRAITS( inch_traits, "inches", "in" )
JM_UNITS_DEFINE_STRINGS_FOR_TRAITS( foot_traits, "feet", "ft" )
}
// Test that the units were defined correctly
#include <type_traits>
static_assert(
std::is_convertible<
custom::megafeet< float >,
custom::centiinches< float >
>::value
&& std::is_convertible<
custom::dozen_inches< float >,
custom::bifeet< float >
>::value,
"feet & inches not mutually convertible"
);
The resulting type of units_linear_relation_lookup( A, B )
is a struct representing how to convert from type B to type A — that is,
A = B * slope_num / slope_den + intercept
Note that only one relationship partial overload is necessary; the library will invert the relationship as needed depending on the direction of the conversion.