Introducing Au: Our open source C++ units library
February 08, 2023 | 5 min. read
By Chip Hogg
Units are mission-critical
When software controls real objects in the physical world, it's critical to get the units right for every variable. This was starkly illustrated by the now-infamous demise of the Mars Climate Orbiter in September 1999. The spacecraft reported its impulse measurements in US customary units as pound-force-seconds, but the trajectory calculator interpreted them in metric (SI) units as newton-seconds—more than four times smaller. Ultimately, this mismeasured steering led to the total loss of the spacecraft, wasting hundreds of millions of dollars.
Like unmanned spacecraft, autonomous vehicles are steered by software. Although we operate on a smaller scale and with a different type of vessel, our work at Aurora is just as critically reliant on accurate computations—which means handling units correctly every time. Fortunately, we have a tool that helps us to get this right, consistently and robustly: our units library, Au.
The units library: A trusted assistant
To understand how the library works, we need a brief refresher on how computer programs are made. The code that programmers write, called "source code," isn't what actually runs on the computer. There's a separate step, called "building" or "compiling," which turns the source code into machine code, i.e., the 1s and 0s that computers can execute.
Units don't show up in the machine code; all we have are numbers. If we wanted to store a speed limit of 65 MPH, it would show up in machine code as simply "65." We'd need to be careful not to pass that value to another part of the program that expects its speeds in meters per second (m/s) because without the context provided by units, the program would assume we are talking about 65 m/s, which is, in fact, over 145 MPH.
The usual way to avoid this kind of error is through painstaking manual labor. Every variable needs to encode its units in its name. For example, we'd call our speed limit variable something like
speed_limit_mph, where the
mph indicates that the units are miles per hour. However, it's safer to use one single system of units everywhere (usually SI). So, more likely, we would immediately convert the speed limit to m/s, and store it as such.
// Defined in some utility file somewhere:
constexpr double MPS_PER_MPH = 0.44704;
// How to store a speed limit in MPH (WITHOUT a units library):
const double speed_limit_mps = 65.0 * MPS_PER_MPH;
When you combine this strategy with a solid safety culture and standard engineering best practices like code review and automated testing, experience shows that it's effective at preventing unit errors. The problem is that it's still a tremendous amount of manual effort. And that work isn't going toward helping the truck drive any better; it's just preventing mistakes that may seem minor, but that would have catastrophic consequences. This is a frustrating situation. If we can automate a Class 8 semitruck delivering goods via the highway, shouldn’t we be able to automate checking the units in our code?
Well, with a units library, we can. Aurora’s Au library gives us a way to "annotate" each variable with its units while we're building source code. The units can then disappear from the machine code, giving us the same executable we would have had before. This means the Aurora Driver can run just as quickly with checked units as it would have without them.
With Au as our trusted assistant, we can be confident that the units have been checked robustly every time we build our code. In fact, if there's a unit error, Au will tell us what the mistake was, so that we can fix it before it causes any harm. Without our units library, our machine code could easily be silently incorrect, which is the kind of insidious error that brings down satellites.
What’s more, when we need a unit conversion, the Au library will generate it automatically. Now we can take all the manual effort that went into checking and re-checking the units, and redeploy it to more interesting problems!
// Unit conversion MPH -> m/s: automatically generated!
const QuantityD<MetersPerSecond> speed_limit =
(miles / hour)(65.0);
// Result: `speed_limit` is `(meters / second)(29.0576)`
Why write our own?
There are many other units libraries for C++, some quite well-established. Before taking the trouble to write our own, we looked for an existing option we could simply reuse. To our surprise and dismay, we couldn't find any library that met all of our needs. Specifically, we needed a library that was:
- Accessible: compatible with a wide variety of C++ versions.
- Developer-friendly: fast to compile, with easy-to-read errors.
- Safe, yet flexible: able to handle a wide variety of numeric types (both floating point and integral), but protected from error-prone conversions and operations.
Many libraries had some of these features, but we needed all of them in a single package. By iterating on Au over the years, we've achieved that, and more.
Au is compatible with the mature, widely available C++14 standard, as well as all of the newer standards. The whole library can be packaged up into a single header file that can simply be dropped into a project folder, no matter which build environment you use. It doesn't get more accessible than that!
To stay developer-friendly, we track our compile times with each proposed API change. This has helped us stay two to three times faster than a leading library we previously tried. Au also has clear, concise error messages. For example, an alternative library describes a variable in a typical error message like this:
units::base_unit<std::ratio<0, 1>, std::ratio<0, 1>,
std::ratio<0, 1>, std::ratio<0, 1>, std::ratio<0, 1>,
std::ratio<0, 1>, std::ratio<0, 1>, std::ratio<0, 1>,
std::ratio<0, 1>>, std::ratio<0, 1>, std::ratio<0, 1>>, double, linear_scale>
It's pretty hard to figure out what unit that represents. With Au, the corresponding type is simply:
This saves significant time and headaches while debugging.
In balancing safety and flexibility, we took a cue from the venerable
std::chrono library. We're very permissive with
double variables, but we prevent any conversion with integral variables unless it’s simply multiplying by an integer. However, Au also goes a step further by protecting against another risk: integer overflow.
The amount of overflow risk depends on the size of the conversion and the range of the type. We take both of these into account in assessing the risk that a conversion will overflow. For example, with a 32-bit integer (whose maximum value is less than 2.2 billion), we'll happily convert
milli(seconds) (a factor of 1,000), but not to
nano(seconds) (a factor of 1 billion)—that's too risky. However, we will convert
nano(seconds) in a 64-bit integer, since that type can hold values of over 9 quintillion. This “safety surface” lets us use a wider variety of integer types safely and with confidence.
Why open source?
One reason is simply to help people who could benefit from what we've already made: a mature, performant, widely accessible units library, forged and refined in the crucible of Aurora's diverse use cases. We didn't set out to write an open source library; we just wanted to meet our own needs, and then share what we had learned. When we did so at CppCon 2021, we found many others whose needs weren't being met by existing alternatives. In conversation after conversation, we saw people struggle with problems we had already solved robustly.
Another reason is to move the C++ units library ecosystem forward. We hope that by releasing Au as open-source, our unique combination of features can make its way to other libraries, making the entire community stronger.
Find us on GitHub
For years now, our units library has been making it easy to handle units safely, and develop our code more quickly. Now, at last, we're sharing it broadly. You can find the library code on GitHub at aurora-opensource/au, and the documentation at aurora-opensource.github.io/au/.
INTERESTED IN BUILDING THE FUTURE OF THE SELF-DRIVING INDUSTRY? WE’RE HIRING! APPLY HERE.