0

I have some code with fairly complicated logic that passing around angles in both radians and degrees. All of the variables are doubles. It would be helpful to add some additional guards to prevent passing a radians to a function that requires the value in degrees. The code below uses a struct and does work but requires .value to get the actual double back. Is it possible to template a primitive without using a struct? Is there a better way of doing this? I'm currently working C++17.

enum class AngleType
{
    Degree,
    Radian
};

template <AngleType T>
struct Angle
{
  double value;
};

void example_function(Angle<AngleType::Radian> angle_radians) { };
J'e
  • 3,014
  • 4
  • 31
  • 55
  • fwiw boost has strong typedefs https://www.boost.org/doc/libs/1_61_0/libs/serialization/doc/strong_typedef.html, though thats maybe a bit old fashioned – 463035818_is_not_an_ai May 27 '21 at 18:52
  • Your approach is fine and it is a typical solution to that problem. For example see how std::chrono uses a very similar approach in order to deal with different time units. – freakish May 27 '21 at 18:55
  • 1
    @463035818_is_not_a_number Why don't you link to the most recent version and the right library, https://www.boost.org/doc/libs/1_76_0/doc/html/boost_units.html ? – EOF May 27 '21 at 18:55
  • I'm already using boost so this sounds promising – J'e May 27 '21 at 18:57
  • @EOF taken from [here](https://stackoverflow.com/a/28916776/4117728). yours isnt the right link to strong typedef either – 463035818_is_not_an_ai May 27 '21 at 18:57
  • @463035818_is_not_a_number No, it's not a link to strong typedef *intentionally*, because boost.units is actually what the OP needs. – EOF May 27 '21 at 18:59
  • A workaround solution is you can create a implicit conversion from `Angle` to `double` – Ranoiaetep May 27 '21 at 19:43

2 Answers2

1

A somewhat common way to do this is provide a conversion operator. Example:

#include <iostream>
#include <math.h>

template<int N, typename T>
struct AngleType
{
    T value;
    AngleType(T val) : value(val) {}

    operator T() const noexcept
    {
        return value;
    }
};

using AngleRadians = AngleType<0, double>;
using AngleDegrees = AngleType<1, double>;

void example_func(AngleRadians angle) {
    std::cout << "angle in radians = " << angle << "\n";
}

int main(int argc, char **argv)
{
    AngleRadians rad = M_PI;
    AngleDegrees deg = 180;
    example_func(rad);
    example_func(deg); // <-- compiler error
}

It has its drawbacks, but it may be good enough for what you're trying to do.

Jon Reeves
  • 2,426
  • 3
  • 14
1

Depends on what you really need, you could actually get rid of Angle and AngleType all together with User-defined literals.

Before starting, you need to decide the base unit you want to use. For my example, I will use radian as base unit.

The idea here is every time you attempt to use a number in degree, it would automatically convert that into radian.

// User-defined literal
constexpr auto operator"" _deg (long double deg)
{
    return deg * PI / 180;
}

constexpr auto operator"" _deg (unsigned long long int deg)
{
    return 1.0_deg * deg;
}

After defining this two, if you want to write a number in degree, you can simply use:

auto a = 90.0_deg;

And it would be equivalent to:

long double a = ((long double)90.0 * PI / 180);

To make it more consistent, you can also define a literal for _rad, and just use:

constexpr auto operator"" _rad (long double rad)
{
    return rad;
}

constexpr auto operator"" _rad (unsigned long long int rad)
{
    return 1.0_rad * rad;
}

Now every time you assign a number to something, you would do:

auto a = 3.14_rad, b = 180_deg;

However, do note that you cannot use literals on variables, so you can't do things like PI_rad. But, since we already settled the base unit as radian, then all variables are stored in radian anyways.

Also note that the parameter for those function are set to long double and unsigned long long int, as they were required by standard.

Ranoiaetep
  • 5,872
  • 1
  • 14
  • 39