Lucky Resistor
Menu
  • Home
  • Learn
    • Learn C++
    • Product Photography for Electronics
      • Required Equipment and Software
    • Soldering for Show
  • Projects
  • Libraries
  • Applications
  • Shop
  • About
    • About Me
    • Contact
    • Stay Informed
  •  
Menu

Units of Measurements for Safe C++ Variables

Posted on 2019-07-112022-09-04 by Lucky Resistor

In your program code, you often deal with unitless values. You can add the units of measurements in the variable or function name, or by adding a comment, but there is still the risk you accidentally make wrong conversions.

I will describe how to handle units of measurements for common time values using a custom template class. I also explain how you use user-defined literals, so you can write values like 100_ms in your code.

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

You require a C++ compiler with support for C++11. This standard is eight years old and should meanwhile be supported by all compilers.

Why are Unitless Values a Problem?

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

void setActionDelay(uint32_t delay);

You are missing important information on this interface. Do you pass seconds, milliseconds, nanoseconds or any other unit of measurements to this function?

Without API documentation, the user has to guess or study the implementation to understand the used unit.

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

I added API documentation to the interface. Now the user of the interface read it and will expect and use milliseconds. They will write code like this:

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

In this user code, the unit is lost again. If you read this code, you have to look up the API documentation realise 1000 actually means 1000 milliseconds.

Decorating Names is no Solution

We can decorate the function and variable names with a suffix, indicating the unit for the value. The resulting interface looks like this:

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

Now we use the interface like this:

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

This is already an improvement compared to the previous example. If you read this code, you assume the value 1000 is milliseconds. You do not have to look up the API documentation.

Nevertheless, this does not prevent another common mistake. If you use a variable to pass the value to the function, this value is still unitless. The compiler can not detect if a nanosecond value suddenly is used as a millisecond value.

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

This code compiles without any errors and also looks valid, but if you test the application, you get unexpected results. 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();

Let’s adjust this interface and also add a suffix to the name of the variable. Next, we have to think about how to convert nanoseconds into milliseconds.

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

Perfect, isn’t it? While testing the new code, you still get unexpected results.

After a while, you realize your error: To convert from nanoseconds to milliseconds you have to divide by 1000000, not just 1000! You wish the compiler had warned you about this issue.

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

Finally… your fixed this code, for now…

With this hypothetical example, I demonstrated several problem areas. Each can accidentally lead to a problem in the code. These problems are not only hard to find, but each change in the code also bears the risk to introduce a new one.

Introducing a Duration Class

As an elegant solution, let’s write a template class Duration to handle all time duration values. It will replace the unitless integer values in our code.

#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.
};

There is already an implementation in the standard library for desktop application development. This implementation is for embedded systems, where you want tight control over the size of your data.

Our goal is to prevent wrong assignments and conversions. The Duration template class is a new numerical type, which acts as a primitive integer, but is bound to one unit.

I removed most comments from the example code to make it more readable. Please write proper API documentation in your code.

There are two template arguments TickType and Ratio. The first argument defines the integer type to count the ticks for the duration. The second defines the ratio between the base unit and the specified unit of the duration.

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 to a primitive integer: All operators will only accept a value of the same unit. You cannot mix one unit with another.

The method ticks() allows direct access to the tick count as a unitless integer value. It will allow converting the duration into a primitive integer to pass it to old functions for compatibility.

I use the constexpr, inline and noexcept keywords to give the compiler hints on how to optimize the code. These will give the compiler as many choices as possible. It will produce the same machine code as using a primitive integer.

In the next step, I declare the actual units:

typedef Duration<uint32_t, std::ratio<1, 1>> Seconds;
typedef Duration<uint32_t, std::milli> Milliseconds;
typedef Duration<uint32_t, std::micro> Microseconds;
typedef Duration<uint32_t, std::nano> Nanoseconds;

First I declare four new types Seconds, Milliseconds, Microseconds and Nanoseconds. Each one with uint32_t as value type and the correct ratio set.

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

constexpr Seconds operator "" _s(unsigned long long int ticks) {
    return Seconds(static_cast<uint32_t>(ticks));
}

constexpr Milliseconds operator "" _ms(unsigned long long int ticks) {
    return Milliseconds(static_cast<uint32_t>(ticks));
}

constexpr Microseconds operator "" _us(unsigned long long int ticks) {
    return Microseconds(static_cast<uint32_t>(ticks));
}

constexpr Nanoseconds operator "" _ns(unsigned long long int ticks) {
    return Nanoseconds(static_cast<uint32_t>(ticks));
}

Using the Duration Class

First, we adjust the interface and remove the decorations from the names. Next, we replace the unitless integer uint32_t with 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 forms: The API documentation and the function syntax.

If we try 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 and if someone else reads the code, they will immediately see you pass a millisecond value to the function.

The new type also solves the second problem with unitless variables:

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

Let’s solve the last problem by adding conversion functions to the class. For example, to convert a value to milliseconds, we add the method toMilliseconds() to 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 generates the correct millisecond value 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 reduce it to a single multiplication or division.

The return type is Duration<TickType, std::milli>, which is the type of Milliseconds we declare in the file. We can not use the declared name at this point, 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 and only change the ratio.

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

This 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 and the converted value.

How to Use the Conversion

We use the new conversion function like this:

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

An interesting side effect using the Duration class: If the unit of getDelayConfig() changes, there is no change required in the user code. 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 firmware. All operations are optimized in the best possible way. The result is like using a primitive integer value.

While there is no performance and size impact, using the Duration class will make your code more readable and safer. 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:

GitHub Repository

Conclusion

You can make your code safer and more readable using unit types. There is no size or performance impact to the compiled firmware. Give it a try, next time you start a new project!

Do you have any questions, did you miss information, or simply want to provide feedback? Add a comment below or ask a question on Twitter!

Learn More

Auto and Structured Binding

Auto and Structured Binding

This article is just a short follow-up article for “Write Less Code using the ‘auto’ Keyword”. Structured binding is something handy, introduced in C++17. Therefore, only the latest compiler will support it. If you are ...
Read More
How and Why to use Namespaces

How and Why to use Namespaces

Namespaces are a feature of C++ which address the problem of name conflicts. There is a "global" namespace, where everything lives which was declared without namespace. Especially the Arduino environment declares a huge amount of ...
Read More
Extreme Integers – Doom from Below

Extreme Integers – Doom from Below

As a beginner or immediate C++ programmer, you heard never mixing unsigned and signed integer types or avoiding unsigned integers at all. There was also this talk about undefined behaviour. Yet, in embedded software development, ...
Read More
C++ Templates for Embedded Code (Part 2)

C++ Templates for Embedded Code (Part 2)

Templates are a powerful feature of the C++ language, but their syntax can be complex. Here I will continue with the second part of the article. Although the examples I provide are for the Arduino ...
Read More
Write Less Code using the "auto" Keyword

Write Less Code using the “auto” Keyword

The auto keyword was introduced with C++11. It reduces the amount of code you have to write, reduces repetitive code and the number of required changes. Sadly, many C++ developers are not aware of how ...
Read More
Real Time Counter and Integer Overflow

Real Time Counter and Integer Overflow

After writing the article about event-based firmware, I realised there are some misunderstandings about how real-time counters are working and should be used. Especially there is a misconception about an imagined problem if such a ...
Read More

Leave a Reply Cancel reply

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

Stay Updated

Join me on Mastodon!

Top Posts & Pages

  • Storage Boxes System for 3D Print
  • Event-based Firmware (Part 1/2)
  • Build a 3D Printer Enclosure
  • Yet Another Filament Filter
  • Circle Pattern Generator
  • Circle Pattern Generator
  • Real Time Counter and Integer Overflow
  • Projects
  • Logic Gates Puzzle 11
  • Units of Measurements for Safe C++ Variables

Latest Posts

  • Better Bridging with Slicer Guides2023-02-04
  • Stronger 3D Printed Parts with Vertical Perimeter Linking2023-02-02
  • Logic Gates Puzzle 1012023-02-02
  • Candlelight Emulation – Complexity with Layering2023-02-01
  • Three Ways to Integrate LED Light Into the Modular Lantern2023-01-29
  • The 3D Printed Modular Lantern2023-01-17
  • Rail Grid Alternatives and More Interesting Updates2022-12-09
  • Large Update to the Circle Pattern Generator2022-11-10

Categories

  • 3D Printing
  • Build
  • Common
  • Fail
  • Fun
  • Learn
  • Projects
  • Puzzle
  • Recommendations
  • Request for Comments
  • Review
  • Software
Copyright (c)2022 by Lucky Resistor. All rights reserved.