Fixing it once and for all: Enforcing units of measure in C++11.

Problem

Game engines are full of stuff like this:

// Defined elsewhere, or in a pre-compiled library.
void rotatef(float angle, float x, float y, float z);
// Another example:
Quaternion fromEuler(float x, float y, float z);

Do these function expect the angle to be in degrees or radians? We’ll look at a few ways you can avoid ambiguities like this from causing subtle, insidious bugs in large projects. Keeping units straight is something that we shouldn’t have to worry about anymore in 2013, but for some reason it’s still a source of very real and very serious bugs in even the most carefully crafted systems.

Solutions

Read the code

You dig up the definition (if you’re lucky enough to have access to it) and try to figure out the implied unit type from the computations it’s used in. For example, if you find that rotatef ends up just wrapping glRotatef then you can surmise that the angle is expected to be in degrees because it’s what glRotatef is defined in the OpenGL documentation to expect.

Pros:

  • Code doesn’t lie.
Cons:
  • You don’t always have access to the source.
  • Tedious, especially if the function is large, or passes the value along to a chain of other functions before it’s actually used.
  • In many contexts it’s not obvious what the unit type is expected to be.
  • No automatic unit enforcement

Documentation and naming conventions


/**
 * @param angleDegrees The rotation angle in degrees.
 * etc...
 */
void rotatef(float angleDegrees, float x, float y, float z);
Pros:
  • The type of the value is described in the function declaration (presuming you include parameter names in your declarations…which I would hope you do) meaning the caller doesn’t have to look any farther.
  • The unit type for the value is clear wherever the value is used.
  • Documentation can be more descriptive about the purpose of the value, as well as its type.
  • Doxygen can rip out all the documentation into a nice web UI
Cons:
  • Not automatic: this solution relies on self-enforced naming and documentation conventions. These conventions could be verified by code reviews, but that takes time and is still prone to errors.
  • Name and usage can fall out of sync. For example:
/**
 * @param angleRadians The rotation in radians
 * etc...
 */
void rotatef(float angleRadians, float x, float y, float z) {
    // glRotatef expects a degrees value as its first argument.
    glRotatef(angleRadians, x, y, z);
    // A very time-consuming bug to track down!
}

Separate classes, implicit conversions, and user-defined float literals

Here we define two classes and non-explicit constructors for converting between the two.

constexpr float PI = 3.1415926535897932;
constexpr float RAD2DEG = 180.0f / PI;
constexpr float DEG2RAD = PI / 180.0f;

class Degrees;

class Radians {
	float value;
public:
	constexpr explicit Radians(float radiansValue) :value(radiansValue) {}
	Radians(const Degrees& degreesValue); // defined below

	constexpr float getValue() const { return value; }
};

class Degrees {
	float value;
public:
	constexpr explicit Degrees(float degreesValue) :value(degreesValue) {}
	Degrees(const Radians& radiansValue) :value(radiansValue.getValue() * RAD2DEG) {}

	constexpr float getValue() const { return value; }
};

inline Radians::Radians(const Degrees& degreesValue) :value(degreesValue.getValue() * DEG2RAD) {}

The constructors taking floats are defined to be explicit for a few reasons. While it’d be convenient to be able to just pass a floating literal to a function taking Degrees, it doesn’t protect against accidentally passing any old float value as an angle value. Making it explicit adds an additional step in which the consumer of our API or function has to confirm that they are, in fact, giving us a proper angle value. Adding some user-defined floating literal suffix conversions allows for the convenience of passing literal values, with the safety of our angle types.

inline Radians operator"" _rad(long double r) { return Radians(r); }
inline Radians operator"" _rad(unsigned long long r) { return Radians(r); }
inline Degrees operator"" _deg(long double d) { return Degrees(d); }
inline Degrees operator"" _deg(unsigned long long d) { return Degrees(d); }

We define conversions for both unsigned long long as well as long double so that values without a decimal point will convert correctly. This allows us to use angle values like so:

#include <iostream>

void foo(const Degrees& d) {
	std::cout << d.getValue() << std::endl;
}

int main() {
	Degrees degrees(180.0f); // Explicit float constructor.
	Radians radians(PI); // Explicit float constructor.

	foo(degrees);
	foo(radians); // Implicit conversion to degrees

	degrees = 180_deg; // Degrees literal
	radians = 3.14159_rad; // Radians literal

	foo(degrees);
	foo(radians); // Implicit conversion to degrees 

	// Define a radians constant.  (Note the shared suffix for clarity)
	constexpr Radians PI_rad = Radians(PI);

	foo(PI_rad); // Implicit conversion to degrees
	foo(180_deg);
}

The output of this code is exactly what we expect:

180
180
180
180
180
180

This is great, but we run into trouble when we try to do more than just pass around single values:

constexpr Radians TWOPI_rad = PI_rad * 2; // ERROR: no operator* defined for class Radians

This error is pretty reasonable. Lets define the operators that make sense for the degrees and radians classes:

class Radians {
	//...
	constexpr Radians operator -() const { return Radians(-value); }
	constexpr Radians operator +(const Radians& rhs) const { return Radians(value + rhs.value); }
	constexpr Radians operator -(const Radians& rhs) const { return Radians(value - rhs.value); }
	constexpr Radians operator *(float rhs) const { return Radians(value * rhs); }
	constexpr Radians operator /(float rhs) const { return Radians(value / rhs); }
	constexpr float   operator /(const Radians& rhs) const 	{ return (value / rhs.value); }
	constexpr friend Radians operator*(float f, const Radians& d) { return Radians(d.value * f); }
	//...
};

These operators allow you to perform basic arithmetic operations on Degrees values:

Degrees d = 4 * (-90_deg + 45_deg - 5_deg) / -2.5f; // d is set to 80_deg

Now we just define the remaining operators that make sense for Degree measurements and the same for Radians, and we've got a robust, powerful tool for preventing unit mismatches.

Pros:
  • Angle unit types are enforced and implicitly converted by the compiler.
  • It doesn't matter what kind of angle value the function takes anymore, you just give it what you have and the conversion is performed if necessary.
  • Passing a float where an angle value was expected causes a compile error.
  • We've encoded an assumption of our system into the type system, allowing for the compiler to enforce it.
  • Still works regardless of documentation or variable naming.
  • Prevents undefined behavior such as multiplying two angles.
  • Implicit conversion saves writing or calling conversion code explicitly.
Cons:
  • Implicit conversion hides complexity. Because the conversion code is inserted by the compiler, it's not obvious where all the conversions are taking place. While implicit conversions are convenient in many cases, it's often better to explicitly convert between the types. This is a matter of programming style, though there's something to be said for maintainability. Given a function that takes Degrees, if we change it to Radians, we have to also change all the callers to convert their Degrees values explicitly to radians, unless we've defined the implicit conversion constructors. This can also have a performance impact if abused, that is if you call multiple functions taking Degrees values with Radians, it will perform the conversion on each call (this may be optimized away). With explicit conversions the caller would be more cognizant of where the conversions are happening and cache off a converted value.
  • Clients must explicitly construct angle values, therefor they must write more code to use the API correctly. This bloat is mitigated by the code saved from implicit conversions, given that the client takes advantage of these features.
  • Slightly slower code than using raw floats. There's (probably) less room for the optimizer to optimize the resulting code. The extent of this slowdown is compiler-dependent.I ran 1Billion angle operations (addition, multiplication, conversion etc..) using the Degrees and Radians classes, and compared the results to running the same operations on raw floating point values and cannot find a measurable difference in performance, unless all optimizations are disabled. Like almost anything performance-related, YMMV. Benchmarks were run on a Macbook pro (2.4 GHz Core 2 Duo, 4GB ram) and compiled with "clang++ -Wall -Wpedantic -std=c++11 -O4"

Conclusions

By creating classes that wrap values of the two angle types, and by defining user-defined literal suffixes, we've shifted the burden of keeping track of units onto the compiler. Not only is this automatically enforced, it's safer, and more explicit than relying on documentation or naming conventions. We can extend this even farther to model the relationship between types like 'meters' and 'seconds' such that 10_m / 5_s; evaluates to an instance of the MetersPerSecond class with the value 2. The full source code for both these classes can be found here along with some examples of their usage.


Update: Thank you @matt_dz for pointing out that since the Degrees and Radians classes are so small, it's better to pass them by value instead of by reference-to-const. A Radians object, since it's the same size as a float, can fit in a single 32bit register (only the range of a float value is specified in the standard, but is very often 4 bytes) when passed by value, allowing more room for the optimizer to cut corners. I'll leave the post as-is and update the github code to reflect this optimization.

Update 2: After benchmarking 1Billion operations using both the pass-by-value and pass-by-const-reference implementations, I cannot find a significant enough difference in performance. I imagine this is due to clang's excellent optimizer replacing the pass-by-const-reference version with a pass-by-value version (or vice-versa) of the classes when it finds that this is faster on the target platform. I also explicitly disabled assignment for everything but non-const lvalue references. This was to prevent bizzarre but valid code like 90_deg = 180_deg; from compiling. As a slightly more realistic example, it prevents something like quaternion.euler().x() = 90_deg; from silently failing.