5
\$\begingroup\$

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 Radians;
class Degrees;

// ========================================================
// class Radians:
// ========================================================

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

    explicit Radians(float radians);
    explicit Radians(const Degrees & degrees);
    Degrees toDegrees() const;

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

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

    // Map an angle in radians to the [0,2pi] range.
    static Radians normalizeAngleTwoPi(float radians);

    // Map an angle in radians to the [-pi,+pi] range.
    static Radians normalizeAnglePi(float radians);

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

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

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

    explicit Degrees(float degrees);
    explicit Degrees(const Radians & radians);
    Radians toRadians() const;

    // 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);
};

// ========================================================
// Radians inline methods:
// ========================================================

inline Radians::Radians(const float radians)
{
    setFloatValue(radians);
}

inline Radians::Radians(const Degrees & degrees)
{
    setFloatValue(degrees.toRadians().angle);
}

inline Degrees Radians::toDegrees() const
{
    return Degrees(angle * RadToDeg);
}

inline float Radians::sin() const
{
    return std::sin(angle);
}

inline float Radians::cos() const
{
    return std::cos(angle);
}

inline float Radians::tan() const
{
    return std::tan(angle);
}

inline Radians Radians::asin(const float sine)
{
    return Radians(std::asin(sine));
}

inline Radians Radians::acos(const float cosine)
{
    return Radians(std::acos(cosine));
}

inline Radians Radians::atan(const float tangent)
{
    return Radians(std::atan(tangent));
}

inline Radians Radians::atan(const float x, const float y)
{
    return Radians(std::atan2(x, y));
}

inline Radians Radians::normalizeAngleTwoPi(float radians)
{
    if (radians >= TwoPi || radians < 0.0f)
    {
        radians -= std::floor(radians * (1.0f / TwoPi)) * TwoPi;
    }
    return Radians(radians);
}

inline Radians Radians::normalizeAnglePi(float radians)
{
    radians = normalizeAngleTwoPi(radians).angle;
    if (radians > Pi)
    {
        radians -= TwoPi;
    }
    return Radians(radians);
}

inline bool Radians::isValidAngle(const float radians)
{
    return std::fabs(radians) <= TwoPi;
}

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

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

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

inline Degrees::Degrees(const Radians & radians)
{
    setFloatValue(radians.toDegrees().angle);
}

inline Radians Degrees::toRadians() const
{
    return Radians(angle * DegToRad);
}

inline float Degrees::sin() const
{
    return std::sin(angle * DegToRad);
}

inline float Degrees::cos() const
{
    return std::cos(angle * DegToRad);
}

inline float Degrees::tan() const
{
    return std::tan(angle * DegToRad);
}

inline Degrees Degrees::asin(const float sine)
{
    return Degrees(std::asin(sine) * RadToDeg);
}

inline Degrees Degrees::acos(const float cosine)
{
    return Degrees(std::acos(cosine) * RadToDeg);
}

inline Degrees Degrees::atan(const float tangent)
{
    return Degrees(std::atan(tangent) * RadToDeg);
}

inline Degrees Degrees::atan(const float x, const float y)
{
    return Degrees(std::atan2(x, y) * RadToDeg);
}

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_radians(Radians(-Pi));
    fn_radians(-3.141592_rad);

    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

    auto rads = Radians(Pi);
    rads *= 1.5f;                     // Multiply by scalar, OK
    rads = rads - Radians(Pi / 2.0f); // Angle difference, OK
//  rads += degs;                     // ERROR, different types of angles
//  rads = rads * rads;               // ERROR, can't multiply angle with angle

//  auto badAngleDegrees = 400.0_deg; // Should trigger an assertion; abs(angle) > 360
//  auto badAngleRadians = 180.0_rad; // Should trigger an assertion; abs(angle) > 2*pi
}

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.

\$\endgroup\$
11
  • \$\begingroup\$ Personally, I think that doing everything in radians makes more sense, but I understand scratching an itch you might have. \$\endgroup\$
    – Edward
    Commented Mar 26, 2015 at 1:44
  • 1
    \$\begingroup\$ @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. \$\endgroup\$ Commented Mar 26, 2015 at 5:23
  • 1
    \$\begingroup\$ @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? \$\endgroup\$ Commented Mar 26, 2015 at 5:27
  • 1
    \$\begingroup\$ 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. \$\endgroup\$
    – glampert
    Commented Dec 7, 2016 at 22:33
  • 1
    \$\begingroup\$ @JackDeeth - It's on GitHub! github.com/glampert/angles \$\endgroup\$
    – glampert
    Commented Dec 10, 2016 at 22:02

2 Answers 2

6
\$\begingroup\$

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.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ 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... \$\endgroup\$
    – glampert
    Commented Mar 26, 2015 at 1:59
  • 3
    \$\begingroup\$ 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. \$\endgroup\$
    – Edward
    Commented Mar 26, 2015 at 2:09
2
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.