Strongly-typed angle measures in radians and degrees

I often work with angles when writing code for 3D rendering and such. Personally, I prefer to measure angles in degrees, but many APIs (including the Standard Library) measure angles in radians. So more than once I have produced bugs by passing an angle in one measure when the function/library was expecting it in another. These kinds of bugs are not hard to track down, but make you waste time nevertheless.

In account of that, I had the idea of defining two classes to represent each angle measure and disable implicit conversions (perhaps inspired by the time durations of std::chono). Following are the Radians and Degrees types (header only):

#ifndef ANGLES_HPP
#define ANGLES_HPP

#include <cmath>
#include <cassert>

constexpr float Pi       = 3.1415926535897931f;
constexpr float TwoPi    = 2.0f * Pi;
constexpr float DegToRad = Pi / 180.0f;
constexpr float RadToDeg = 180.0f / Pi;

//
// Implementation details:
//
namespace internal
{

// ========================================================
// template class AngleOps<DERIVED>:
// ========================================================

// Operators and methods shared by both Degrees and Radians classes.
template<class DERIVED>
class AngleOps
{
public:

//
// Add, subtract and negate angles:
//

DERIVED operator - () const { return DERIVED(-angle); }

DERIVED operator + (const DERIVED & other) const { return DERIVED(angle + other.angle); }
DERIVED operator - (const DERIVED & other) const { return DERIVED(angle - other.angle); }

DERIVED & operator += (const DERIVED & other) { setFloatValue(angle + other.angle); return static_cast<DERIVED &>(*this); }
DERIVED & operator -= (const DERIVED & other) { setFloatValue(angle - other.angle); return static_cast<DERIVED &>(*this); }

//
// Multiply and divide angle by a scalar value:
//

DERIVED operator * (const float scalar) const { return DERIVED(angle * scalar); }
DERIVED operator / (const float scalar) const { return DERIVED(angle / scalar); }

DERIVED & operator *= (const float scalar) { setFloatValue(angle * scalar); return static_cast<DERIVED &>(*this); }
DERIVED & operator /= (const float scalar) { setFloatValue(angle / scalar); return static_cast<DERIVED &>(*this); }

//
// Expose the built-in comparison operators:
//

bool operator == (const DERIVED & other) const { return angle == other.angle; }
bool operator != (const DERIVED & other) const { return angle != other.angle; }
bool operator <= (const DERIVED & other) const { return angle <= other.angle; }
bool operator >= (const DERIVED & other) const { return angle >= other.angle; }
bool operator  < (const DERIVED & other) const { return angle  < other.angle; }
bool operator  > (const DERIVED & other) const { return angle  > other.angle; }

//
// Access the underlaying scalar value:
//

float getFloatValue() const
{
return angle;
}

void setFloatValue(const float ang)
{
assert(DERIVED::isValidAngle(ang) &&
"Value is not in a valid range to be used as a degrees or radians amount!");
angle = ang;
}

protected:

AngleOps() : angle(0.0f) { }

// The actual angle. Interpreted as radians or degrees,
// depending on the class that implements this.
float angle;
};

} // namespace internal {}

class Degrees;

// ========================================================
// ========================================================

{
public:

Degrees toDegrees() const;

// Sine/cosine/tangent of this angle:
float sin() const;
float cos() const;
float tan() const;

// To angle:
static Radians atan(float x, float y);

// Map an angle in radians to the [0,2pi] range.

// Map an angle in radians to the [-pi,+pi] range.

// Test if an (absolute) float value is in the [0,2pi] range.
};

// ========================================================
// class Degrees:
// ========================================================

class Degrees final
: public internal::AngleOps<Degrees>
{
public:

explicit Degrees(float degrees);

// Sine/cosine/tangent of this angle:
float sin() const;
float cos() const;
float tan() const;

// To angle:
static Degrees asin(float sine);
static Degrees acos(float cosine);
static Degrees atan(float tangent);
static Degrees atan(float x, float y);

// Map an angle in degrees to the [0,360] range.
static Degrees normalizeAngle360(float degrees);

// Map an angle in degrees to the [-180,+180] range.
static Degrees normalizeAngle180(float degrees);

// Test if an (absolute) float value is in the [0,360] range.
static bool isValidAngle(float degrees);
};

// ========================================================
// ========================================================

{
}

{
}

{
}

{
return std::sin(angle);
}

{
return std::cos(angle);
}

{
return std::tan(angle);
}

{
}

{
}

{
}

{
}

{
{
}
}

{
{
}
}

{
}

// User defined suffix _rad for literals with type Radians (C++11).
{
// Note: The standard requires the input parameter to be long double!
}

// ========================================================
// Degrees inline methods:
// ========================================================

inline Degrees::Degrees(const float degrees)
{
setFloatValue(degrees);
}

{
}

{
}

inline float Degrees::sin() const
{
}

inline float Degrees::cos() const
{
}

inline float Degrees::tan() const
{
}

inline Degrees Degrees::asin(const float sine)
{
}

inline Degrees Degrees::acos(const float cosine)
{
}

inline Degrees Degrees::atan(const float tangent)
{
}

inline Degrees Degrees::atan(const float x, const float y)
{
}

inline Degrees Degrees::normalizeAngle360(float degrees)
{
if (degrees >= 360.0f || degrees < 0.0f)
{
degrees -= std::floor(degrees * (1.0f / 360.0f)) * 360.0f;
}
return Degrees(degrees);
}

inline Degrees Degrees::normalizeAngle180(float degrees)
{
degrees = normalizeAngle360(degrees).angle;
if (degrees > 180.0f)
{
degrees -= 360.0f;
}
return Degrees(degrees);
}

inline bool Degrees::isValidAngle(const float degrees)
{
return std::fabs(degrees) <= 360.0f;
}

// User defined suffix _deg for literals with type Degrees (C++11).
inline Degrees operator "" _deg (long double degrees)
{
// Note: The standard requires the input parameter to be long double!
return Degrees(static_cast<float>(degrees));
}

#endif // ANGLES_HPP


Here is a snippet just to illustrate how it works:

void fn_radians(const Radians &) { }
void fn_degrees(const Degrees &) { }

int main()
{

fn_degrees(Degrees(128.0f));
fn_degrees(128.0_deg);

auto degs = Degrees(360.0f);
degs -= 1.0_deg;                  // Minus one degree, OK
degs /= 2.0f;                     // Divide angle by two, OK
degs = degs + 60.0_deg;           // Add a few more degrees, OK
degs = degs * 0.2f;               // Multiply by scalar, OK
//  degs += 128.0f;                   // ERROR, can't add angle with scalar

rads *= 1.5f;                     // Multiply by scalar, OK
//  rads += degs;                     // ERROR, different types of angles

//  auto badAngleDegrees = 400.0_deg; // Should trigger an assertion; abs(angle) > 360
}


So what do you guys think? Is this a good idea, or just a waste of time? I'm I on the right track?

Other concerns I have:

• Any corner cases I'm missing on implicit conversions that might break the whole concept of strongly typed angle measures?

• What do you think of being able to multiply by a scalar? AFAIK, you can't multiply angles with angles, but being able to scale an angle is quite useful sometimes.

• Any other comments on style and correctness are very welcome.

• Personally, I think that doing everything in radians makes more sense, but I understand scratching an itch you might have. Commented Mar 26, 2015 at 1:44
• @Edward: Mathematically I agree radians make more sense. But in terms of accurate (not lossy) storage of an angle degrees (because it can be represented by an integer) makes more sense (especially if you store it as degree * 10,000,000. You can store the angle to 7 places of precision without loosing information). Storing it as a float you always need to step back and think which way is the epsilon going to round it. In the end radions is probably the default for me also but it takes thinking first. Commented Mar 26, 2015 at 5:23
• @glampert: Why not store degrees as an integer (*10,000,000) to avoid unexplained rounding errors. Or store it as 3 int values degrees/minutes/seconds as three discrete values? Commented Mar 26, 2015 at 5:27
• Hi @JackDeeth. I wrote this a long time ago, had forgotten I had posted here ;) Yes, I can clean it up and put on github, can probably do that next weekend. Maybe it should then also be templated on the scalar type - for more generic library usage, I don't think it is a good idea to hardcode it to use float, some might not care about size and want the full precision of a double. Commented Dec 7, 2016 at 22:33
• @JackDeeth - It's on GitHub! github.com/glampert/angles Commented Dec 10, 2016 at 22:02

I think it's generally well written, but there are a few things that concern me. In particular:

Consider throwing exceptions rather than asserts

If we start with 360.0 degrees and add a degree, I'm not sure I'd be happy with an assert versus throwing an exception if I were using these classes. It seems to me that I'd more likely want to handle the condition myself, or even better, be able to set a flag as to whether it's an error condition or not.

Use M_PI rather than defining your own

Rather than defining Pi you could simply use M_PI which is defined in math.h (aliased to cmath which you already include).

Consider renaming namespace internal

While it might be descriptive for this one and only instance, internal is an awfully generic name to use for a namespace. Consider using something more descriptive.

• Thanks for the input! M_PI is available on all compilers I can remember, but it doesn't seem to be standard. This answer suggest it is best to define your own, but I don't have a strong opinion on that... Commented Mar 26, 2015 at 1:59
• Technically speaking, M_PI is a Posix rather than a C or C++ standard, but I'd suggest that at the very least, it would be useful to check to see if M_PI is already defined before using a possibly lower precision result. Commented Mar 26, 2015 at 2:09

The idea doesn't look appealing to me. One only cares about degrees while dealing with literals, and IO. Degrees are not very well suited for calculations anyway.

Literals are well-addressed with the user-defined suffix.

IO is not addressed at all. Extending ios/iomanip could be a step in the right direction.

Multiplication by a scalar is well-defined mathematically, and I don't see the reason why it should not be supported.

Angle multiplication per se is quite unusual; on the other hand, think of Tailor series for $sin$ and friends.