How to use Unit Types for Safe and Readable Code

In your program code, you often have to deal with values which have a given unit. The problem is, this unit is often completely lost. If you do not add the unit as a comment or as part of the variable name, there is the risk you accidentally make wrong conversions.

This article describes how you declare new types to handle the units for common time values. It also explains how to use user defined literals so you can write values like 100_ms in your code.

The shown example code will only work with 32-bit processors, like the SAM D21 used in the Arduino Zero or Adafruit Feather M0 platform. It is possible to adapt the code to compile on the AVR toolchain for Arduino Uno and similar, but this is not part of this article.

Another requirement is your C++ compiler has to support the language standard C++11. This standard is eight years old and should meanwhile be supported by all compilers.

What is the Issue not Using Units?

Imagine an interface where you set the delay of some action:

void setActionDelay(uint32_t delay);

The problem with this interface is the lack of unit information. Do you pass seconds, milliseconds, nanoseconds or any other unit to this function? If you do not document the function, the user has to guess or study the implementation of the function.

/// Set the Delay for the action.
///
/// @param delay The delay in milliseconds.
///
void setActionDelay(uint32_t delay);

Much better. Now the interface user can read the API documentation and is aware you expect milliseconds. They will start using the code like this:

void setup() {
  // ...
  setActionDelay(1000);
  // ...
}

Again, the unit is lost. If someone else is reading this code, they have to look up the API documentation to find out about the millisecond unit and to know, 1000 means 1000 milliseconds.

Decorate Names is Ugly

A standard solution is to decorate function and variable names. The resulting interface looks like this:

/// Set the Delay for the action.
///
/// @param delay The delay in milliseconds.
///
void setActionDelayMs(uint32_t delay);

Now the user code will read like this:

void setup() {
  // ...
  setActionDelayMs(1000);
  // ...
}

It is an improvement. If someone else is reading the code, he can assume 1000 is milliseconds, without looking up the API documentation.

It does not prevent another common mistake I often find in poorly written code:

void setup() {
  // ...
  const auto delay = getDelayConfig();  
  setActionDelayMs(delay);
  // ...
}

Compiles fine and also looks good, but there is a problem testing the application. Reading the API documentation of getDelayConfig() resolves the source of the issue:

/// Get the configured delay for actions.
///
/// @return The delay in nanoseconds.
///
uint32_t getDelayConfig();

So, let us adjust this interface as well and also the name of the variable. Next, we have to think, how we can convert nanoseconds into milliseconds carefully.

void setup() {
  // ...
  const auto delayNs = getDelayConfigNs();  
  setActionDelayMs(delayNs / 1000);
  // ...
}

Perfect, isn’t it? But testing still raises a problem.

After a while, you realize, to convert from nanoseconds to milliseconds, you have to divide by 1000000, not just 1000.

void setup() {
  // ...
  const auto delayNs = getDelayConfigNs();  
  setActionDelayMs(delayNs / 1000000);
  // ...
}

Finally…

This hypothetical example visualizes several problem areas. Each can accidentally lead to a problem in the code which is hard to find.

Introducing a Duration Class

There is an elegant solution for all the shown problems. Instead of using primitive integers, we write a class Duration which will handle any time duration values.

#include <cstdint>
#include <ratio>

template<typename TickType, typename Ratio>
class Duration
{
public:
    constexpr explicit Duration(TickType ticks) noexcept : _ticks(ticks) {}
    
public: // Operators
    constexpr inline bool operator==(const Duration &other) const noexcept { return _ticks == other._ticks; }
    constexpr inline bool operator!=(const Duration &other) const noexcept { return _ticks != other._ticks; }
    constexpr inline bool operator>(const Duration &other) const noexcept { return _ticks > other._ticks; }
    constexpr inline bool operator>=(const Duration &other) const noexcept { return _ticks >= other._ticks; }
    constexpr inline bool operator<(const Duration &other) const noexcept { return _ticks < other._ticks; }
    constexpr inline bool operator<=(const Duration &other) const noexcept { return _ticks <= other._ticks; }
    constexpr inline Duration operator+(const Duration &other) const noexcept { return Duration(_ticks+other._ticks); }
    constexpr inline Duration operator-(const Duration &other) const noexcept { return Duration(_ticks-other._ticks); }

public:
    constexpr inline TickType ticks() const noexcept { return _ticks; }
    
private:
    TickType _ticks; ///< The number of ticks.
};

If you are developing desktop application, please note this is most likely already implemented in the standard library for you. It is a special implementation for embedded systems.

The goal of the Duration template class is to provide a new type, which prevents wrong assignments to variables and functions but compiles like any primitive integer. I removed most comments from the example code to make it more readable.

There are two template arguments TickType and Ratio. The first argument defines the integer type to use to count the ticks for the duration. The second defines the ratio between the base unit and the specified unit of the duration. This ratio is not used in this article, you can use it later to extend the class with automatic unit conversion functions.

A single constructor takes the number of ticks and a complete set of operators make sure you can use the duration like any other integer value. The only difference is, all operators will only work with a value of the same unit, you can not mix one unit with another.

The methods ticks() allow direct access to the tick count as unit less integer value. It will allow converting the duration into a primitive integer to pass it to regular functions.

I use constexpr, inline and noexcept to give the compiler some hints how to optimize the code. It can work with the variables directly, and there is no need to create functions in the machine code.

In the next step, the actual units are declared:

typedef Duration<uint32_t, std::ratio<1, 1>> Seconds;
constexpr Seconds operator "" _s(unsigned long long int ticks) {
    return Seconds(static_cast<uint32_t>(ticks));
}

typedef Duration<uint32_t, std::milli> Milliseconds;
constexpr Milliseconds operator "" _ms(unsigned long long int ticks) {
    return Milliseconds(static_cast<uint32_t>(ticks));
}

typedef Duration<uint32_t, std::micro> Microseconds;
constexpr Microseconds operator "" _us(unsigned long long int ticks) {
    return Microseconds(static_cast<uint32_t>(ticks));
}

typedef Duration<uint32_t, std::nano> Nanoseconds;
constexpr Nanoseconds operator "" _ns(unsigned long long int ticks) {
    return Nanoseconds(static_cast<uint32_t>(ticks));
}

First I declare new types Seconds, Milliseconds, Microseconds and Nanoseconds with a uint32_t as integer value and with the correct ratio set.

Next, I declare a user-defined literal suffix _s, _ms, _us and _ns for all these types.

Using the Duration Class

First, we can adjust the interface and remove the decorations from the names. Instead of the uint32_t we can use the new Milliseconds and Nanoseconds types:

/// Set the Delay for the action.
///
/// @param delay The delay in milliseconds.
///
void setActionDelay(Milliseconds delay);

/// Get the configured delay for actions.
///
/// @return The delay in nanoseconds.
///
Nanoseconds getDelayConfig();

The interface is now documented in two ways: First, with the API documentation comment and second by the function syntax.

If a user tries to use these functions with primitive integer literals, the compiler will stop with an error:

void setup() {
  // ...
  setActionDelay(1000);
  // ...
}

The compiler will complain like this:

duration.ino: In function 'void setup()':
duration:29:22: error: could not convert '1000' from 'int' to 'Milliseconds {aka Duration<long unsigned int, std::ratio<1ll, 1000ll> >}'
   setActionDelay(1000);

To assign a literal value, you have to use the correct suffix:

void setup() {
  // ...
  setActionDelay(1000_ms);
  // ...
}

It will compile without issue. If someone else reads the code, they will immediately see you pass a millisecond value to the function.

The new types also solve the second problem we had:

void setup() {
  // ...
  const auto delay = getDelayConfig();  
  setActionDelay(delay);
  // ...
}

The compiler will stop with an error:

duration.ino: In function 'void setup()':
duration:29:23: error: could not convert 'delay' from 'const Duration<long unsigned int, std::ratio<1ll, 1000000000ll> >' to 'Milliseconds {aka lr::Duration<long unsigned int, std::ratio<1ll, 1000ll> >}'
   setActionDelay(delay);

Adding Conversion Functions

To solve the last problem, we can easily add conversion functions to the class. For example, to convert to milliseconds, you have to add this method of the Duration class:

/// Convert to Milliseconds.
///
constexpr inline Duration<TickType, std::milli> toMilliseconds() const noexcept {
  typedef std::ratio_divide<Ratio, std::milli> r;
  return Duration<TickType, std::milli>(_ticks * r::num / r::den);
} 

This method will automatically generate the correct conversion to milliseconds for any defined unit at compile time. If you are not used to working with templates, this function may look to you like some voodoo. Nevertheless, it is straightforward to explain:

The modifiers constexpr and inline tell the compiler to generate the code directly inline and do not create an actual function for this simple conversion. In the end, it will be a simple multiplication or division.

The return type is Duration<TickType, std::milli>, which is just the written out type of Milliseconds we declare later in the file. We can not use the declared name, because it is declared after Duration and is based on the Duration class. For the first template argument, we reuse TickType to use the same integer but just a different ratio.

In the method, the first statement is typedef std::ratio_divide<Ratio, std::milli> r; which will divide the ratio of the current template class with the ratio std::milli of the result. By dividing the two ratios, I will get the correct conversion factor.

This conversion factor is applied to the current value with the expression _ticks * r::num / r::den. This result is used to construct a new Duration object with the desired ratio.

How to Use the Conversion

The new conversion function is straightforward to use:

void setup() {
  // ...
  const auto delay = getDelayConfig();  
  setActionDelay(delay.toMilliseconds());
  // ...
}

An interesting side effect of using our new Duration class, if the unit of getDelayConfig() changes, there is no change in the user code required. The method toMilliseconds() will convert any Duration unit to milliseconds.

Is there a Performance or Size Impact?

The Duration class has no performance or size impact on the code. All operations are converted to simple operations which are optimized in the best possible way.

While there is no performance and size impact, using the Duration class will make your code more readable and safer, because any unit mismatch in the code will lead to a compiler error.

Example Code

You can find the example code from this article in the following GitHub repository:

https://github.com/LuckyResistor/unit-type-example

Learn More

Conclusion

Using unit types makes your code safer and more readable, without any size or performance impact. Therefore, if you start a new project, give it a try!

If you have questions, miss some information or have any feedback, feel free to add a comment below.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.