Linear speed to revolutions per minute¶
A wheel rolls along the ground at some speed. How fast is it turning, in revolutions per minute?
// Speed must be m/s. Radius must be meters. Returns RPM.
float wheel_rpm(float v_mps, float r_m) {
return v_mps / (2.0f * static_cast<float>(M_PI) * r_m) * 60.0f;
}
int main() {
// The wheel radius is 350 mm, so convert it to meters by hand first.
const float omega_rpm = wheel_rpm(15.0f, 350.0f / 1000.0f);
std::cout << omega_rpm << " rev / min" << '\n'; // Unit label typed by hand.
}
Includes and usings
#include "au/au.hh"
#include <iostream>
#include "au/io.hh"
#include "au/units/meters.hh"
#include "au/units/minutes.hh"
#include "au/units/radians.hh"
#include "au/units/revolutions.hh"
#include "au/units/seconds.hh"
// This is a `.cc` file, so we import the names we use, one at a time. See the "Namespaces and
// includes" discussion page for why we do this rather than `using namespace au;`.
using au::Meters;
using au::meters;
using au::milli;
using au::minute;
using au::Minutes;
using au::QuantityF;
using au::Revolutions;
using au::revolutions;
using au::second;
using au::Seconds;
using au::UnitQuotient;
using au::symbols::rad;
// Aliases for the compound units, so the signature below reads well. A type alias introduces one
// name we chose, so it is fine at namespace scope even in a header.
using Rpm = UnitQuotient<Revolutions, Minutes>;
using MetersPerSecond = UnitQuotient<Meters, Seconds>;
// The types state the units. Nothing to remember; nothing to convert.
QuantityF<Rpm> wheel_rpm(QuantityF<MetersPerSecond> v, QuantityF<Meters> r) {
return v * rad / r;
}
int main() {
const auto omega = wheel_rpm((meters / second)(15.0f), milli(meters)(350.0f));
std::cout << omega.as(revolutions / minute) << '\n';
}
Both programs print 409.256 rev / min1. Note where that unit label comes from: the Au version
derives it from the return type, while the raw version has it typed in by hand: one more thing to
keep in sync.
What’s happening¶
The raw version computes \omega = \frac{v}{2 \pi r} \cdot 60. The underlying physics gets obscured by manual unit bookkeeping:
-
2\pi converts the angle units between radians and revolutions
-
60 converts the time units between seconds and minutes
It’s up to whoever writes this function to get these constants right, and to make sure they don’t accidentally multiply when they should divide (or vice versa).
The real vulnerability is in the interface. The function needs very specific units, but they
are only ever described — in a comment, and in the parameter name suffixes (_mps and _m).
Nothing there is checked. The callers will be in some other file, far away, where neither the
comment nor the parameter names are easy to see. Any mistakes will be hard to spot, and the
compiler will let them right on through.
Au tackles this vulnerability head-on. With QuantityF parameters, we know that users can only
pass the right kind of quantity: the compiler will produce a readable error if they get it wrong.
We even get extra flexibility: if we change the units at the callsite, the compiler will generate
a correct and efficient conversion factor automatically, and we’ll still get the right answer!
We’ve seen this in action in the example above, where we pass a millimeter length to a function that
takes the length in meters.
The implementation gets a lot simpler, too. All of the magic number conversion factors are gone. Instead, we just state the underlying physics directly and clearly:
- angular velocity is proportional to linear velocity
- the conversion factor is just a ratio between angle and length
- “radian per radius” (
rad / rin code) is exactly that ratio, by definition of the radian unit.
It’s instructive to consider what’s happening on the C++ level in the v * rad / r expression.
v and r are both QuantityF types, so they store a float under the hood. But rad is
different: it’s a unit symbol, which means it takes up no space at all, and never consumes
a runtime instruction. Multiplying by rad only relabels the units, at compile time. So if we
looked at the assembly generated for v * rad / r, we would see only one float divided by
another.
Of course, v * rad / r is not expressed in revolutions per minute, but in radians per second.
To close the loop, Au compares this to the return type, and automatically generates, at compile
time, the single floating point number that combines all these conversion factors. Thus, the
only further runtime instruction is a single multiplication.
That is two floating point instructions in total, against three for the raw version, which spends
one on 2 * PI * r before it can divide.
To summarize the overall comparison:
-
Runtime cost: Au is generally at least as good as the raw version. In fact, in this specific case, it’s even slightly better (two floating point instructions vs three).
-
Conversion factor accuracy: Same in both cases, except Au’s version is guaranteed to be correct, with no maintenance cost.
-
Safety: Au’s version is both vastly safer (guaranteeing callers pass the right dimension), and more flexible (letting callers use any units they want).
Related reading¶
- Unit symbols, such as the
radused here. - Quantity, and how conversions are applied.
- Types for combined units, for expressions like
UnitQuotient<Revolutions, Minutes>. - Namespaces and includes, including why a
.ccimports names individually and a.hhdoes not.
-
All of our examples get compiled and run in CI, and we check that they produce the same output. ↩