Arene Base
Fundamental Utilities For Safety Critical C++
Loading...
Searching...
No Matches
units: Facilities For Representing Values With Units

Arene base provides facilities for working with values that represent quantities with units.

The public header is:

Public export header for the units subpackage.

The Bazel target is

//:units

Introduction

The units sub-package provides facilities for working with values that represent quantities with units.

Quantities

At the basic level, the arene::base::quantity class template is intended to represent an amount of a physical quantity, such as the distance in millimetres to a nearby object, the road speed of a car in miles per hour, or the rotational speed of a car engine in RPM. Quantities measuring different things cannot be assigned to each other, and quantities representing the same thing but with different units are automatically scaled during assignment, thus preventing a category of bugs where incorrect calculation results are assigned to a variable, or there is a mismatch in units between one module and another.

arene::base::quantity takes two template parameters.

The first template parameter is the physical quantity type of the value. This specifies what the value represents, and is a tag type such as wheel_rotation_speed_in_rpm, or vehicle_speed_in_mph. This is used to ensure that measurements of different things cannot accidentally be assigned to each other, and measurements of the same thing in different units are appropriately scaled when assigned. No instances of this tag type are ever instantiated.

The second template parameter is the representation type: the actual type used to store the value. It defaults to double, but can be any arithmetic type, including class types with arithmetic operations. This allows developers to use arene::base::quantity as a direct replacement for any variable that would otherwise have stored a physical quantity value, maintaining the same storage type.

Avoiding errors

The primary purpose of the units sub-package is to avoid errors when writing code. The use of distinct tag types for distinct measurements, or for your measurements in different units, avoids accidental errors. If latitude and longitude are different types, then it is impossible to get them confused, whereas if they are merely double values, then it is easy to pass them in the wrong order to a function.

Type aliases are error-prone

Suppose we have a geographic_position which is made up of a latitude and a longitude, and a make_position function to construct one. Since the fields have the same type, it is easy to get them in the wrong order: the compiler won't complain if we construct the geographic_position as {longitude, latitude} even though the fields are in the opposite order.

/// A type for latitude measurements
using latitude_t = double;
/// A type for longitude measurements
using longitude_t = double;
/// A struct representing a geographic position
struct geographic_position {
/// The latitude
latitude_t latitude;
/// The longitude
longitude_t longitude;
};
/// @brief Construct a geographic position from latitude and longitude
/// @param latitude The latitude in degrees
/// @param longitude The longitude in degrees
/// @returns geographic_position with the specified latitude and longitude
geographic_position make_position(latitude_t latitude, longitude_t longitude) { return {latitude, longitude}; }

The same applies to function parameters: the compiler will merrily accept the latitude and longitude values in the wrong order, even though we have carefully checked the values, and used the correct type aliases.

// error latitude and longitude are in the wrong order
latitude_t eiffel_tower_latitude = 48.8584;
longitude_t eiffel_tower_longitude = 2.2945;
auto eiffel_tower_position = make_position(eiffel_tower_longitude, eiffel_tower_latitude);

Quantity types prevent errors

Using arene::base::quantity prevents these errors:

/// A tag type for latitude measurements
struct latitude_in_degrees {};
/// A tag type for longitude measurements
struct longitude_in_degrees {};
/// A struct representing a geographic position
struct geographic_position {
/// The latitude
arene::base::quantity<latitude_in_degrees> latitude;
/// The longitude
arene::base::quantity<longitude_in_degrees> longitude;
};
/// @brief Construct a geographic position from latitude and longitude
/// @param latitude The latitude in degrees
/// @param longitude The longitude in degrees
/// @returns geographic_position with the specified latitude and longitude
geographic_position make_position(
arene::base::quantity<latitude_in_degrees> latitude,
arene::base::quantity<longitude_in_degrees> longitude
) {
return {latitude, longitude};
}

By using arene::base::quantity with distinct physical quantity types for the fields, it is very hard to construct an instance with the wrong fields, as the compiler will issue a compilation error.

Similarly, if we were to accidentally reverse the arguments to make_position when constructing the location of the Eiffel Tower as below, then the compilation would fail.

auto eiffel_tower_position = make_position(
arene::base::quantity<latitude_in_degrees>{48.8584},
arene::base::quantity<longitude_in_degrees>{2.2945}
);

Conversions

By default, arene::base::quantity types with different physical quantity types are treated as entirely independent things. This is important: we don't want quantity<distance> to be convertible to quantity<time>.

However, sometimes we do want conversions: quantity<time_in_hours> should be convertible to quantity<time_in_seconds>, with appropriate scaling of the stored value (multiply by 3600).

This is achieved by specializing arene::base::units_conversion_traits for the two types. Setting the compatible member to true indicates that the quantities can be implicitly converted from the first physical quantity type to the second. The scale_factor member then specifies the scaling to apply.

/// A tag type for time measured in hours
struct time_in_hours {};
/// A tag type for time measured in seconds
struct time_in_seconds {};
/// @brief Specialization of @c units_conversion_traits for converting @c time_in_hours to @c time_in_seconds
/// @tparam From the type of the unit being "converted"
template <>
struct arene::base::units_conversion_traits<time_in_hours, time_in_seconds> {
/// @brief Boolean value that is @c true to indicate that @c time_in_hours can be converted to @c time_in_seconds
ARENE_MAYBE_UNUSED static bool const compatible = true;
/// @brief The scale factor to use when converting @c time_in_hours to @c time_in_seconds
using scale_factor = std::ratio<3600, 1>;
};
/// @brief Specialization of @c units_conversion_traits for converting @c time_in_hours to @c time_in_seconds
/// @tparam From the type of the unit being "converted"
template <>
struct arene::base::units_conversion_traits<time_in_seconds, time_in_hours> {
/// @brief Boolean value that is @c true to indicate that @c time_in_seconds can be converted to @c time_in_hours
ARENE_MAYBE_UNUSED static bool const compatible = true;
/// @brief The scale factor to use when converting @c time_in_seconds to @c time_in_hours
using scale_factor = std::ratio<1, 3600>;
};

Here, we specialize both directions because we want conversion to be permitted in both directions. One hour is 3600 seconds, and 1 second is 1/3600 of an hour, so the scale factors are set appropriately.

Implicit conversions then work as expected:

static_assert(secs.count_of<time_in_seconds>() == (42 * 3600), "Value must be right");
constexpr arene::base::quantity<time_in_hours> hours2 = secs;
static_assert(hours2 == hours, "Value must be right");

The upcoming Units Framework will simplify the specification of relationships between physical quantity types so manual specialization of arene::base::units_conversion_traits is not required.

Comparisons

Any two arene::base::quantity types can be compared for equality, ordering, and three-way-comparison if they have the same physical quantity types, or one can be converted to the other.

constexpr arene::base::quantity<time_in_seconds> secs2 = secs * 2;
static_assert(hours < secs2, "Value must be right");

You cannot compare quantities with different physical quantity types if they are not interconvertible:

static_assert(
!arene::base::
is_equality_comparable_v<arene::base::quantity<time_in_hours>, arene::base::quantity<distance_in_metres>>,
"Cannot compare time to distance"
);

Arithmetic

By default, you can add and subtract two arene::base::quantity instances with the same physical quantity type, yielding another quantity of the same type, with the sum or difference of the values. However, you cannot add or subtract quantities with different physical quantity types, since these are assumed to be unrelated.

static_assert(sec3.count_of<time_in_seconds>() == 141, "Value must be right");
static_assert((sec3 - sec2) == sec1, "Value must be right");
// auto error = distance + sec1; // would be a compilation error

By default, instances of arene::base::quantity can be multiplied or divided by a scalar value, yielding a new arene::base::quantity holding the resulting value, but arene::base::quantity values cannot be multiplied by other arene::base::quantity values, even if they have the same physical quantity type. This is because multiplying two lengths should yield an area, but until the Units Framework is ready, the library cannot determine the physical quantity type of the result.

You can manually control the result of adding, subtracting, multiplying and dividing quantities by specializing arene::base::units_combination_traits.

/// A tag type representing an area in metres squared
struct area_in_metres_squared {};
/// A tag type representing a speed in metres per second
struct speed_in_metres_per_second {};
/// Specialization showing the supported operations on two distances: adding, subtracting and multiplying
template <>
struct arene::base::units_combination_traits<distance_in_metres, distance_in_metres> {
/// The result of adding two distances is a distance
using sum_type = distance_in_metres;
/// The difference between two distances is a distance
using difference_type = distance_in_metres;
/// The product of two distances is an area
using product_type = area_in_metres_squared;
};
/// Specialization showing the supported operations on a distance and a time: only division to create a speed is
/// supported
template <>
struct arene::base::units_combination_traits<distance_in_metres, time_in_seconds> {
/// Dividing a distance by a time yields a speed
using ratio_type = speed_in_metres_per_second;
};

The upcoming Units Framework will simplify the specification of relationships between physical quantity types so manual specialization of arene::base::units_combination_traits is not required.

Units Framework

A Units Framework is under development, which will provide better ways of declaring the relationship between physical quantity types, so that manual specialization of arene::base::units_conversion_traits and arene::base::units_combination_traits is unnecessary.