gone
gone

Reputation: 1129

C++ constructor, one for degrees, one for radians

What would be the best practice in C++, to define two separate constructors, one having input in degrees whilst the other in radians? The problem I'm having doing it the straight forward way is that the signature of both methods look the same to the compiler, even though the parameter names identify which is which, and the compiler flags a re-declaration error. Is this possible to do without introducing another field?

I can do it using a single constructor by adding an additional bool parameter that would allow selecting which units are being passed, and using an if in the body.

Upvotes: 10

Views: 2869

Answers (6)

Aluan Haddad
Aluan Haddad

Reputation: 31873

I suggest an approach such as

const auto PI = 22 / 7.0;

const struct Degrees {
    explicit constexpr Degrees(double value): value{value} {}
    const double value;
};
const struct Radians {
    explicit constexpr Radians(double value): value{value} {}
    const double value;
};

const struct Angle {
    constexpr Angle(Radians r) : radians{r} {}
    constexpr Angle(Degrees d) : Angle{Radians{180.0 * (d.value / PI)}} {}
    const Radians radians;
};

decltype(auto) operator <<(std::ostream& out, const Degrees& d) {
    return out << d.value << " degrees";
}
decltype(auto) operator <<(std::ostream& out, const Radians& r) {
    return out << r.value << " radians";
}
decltype(auto) operator <<(std::ostream& out, const Angle& a) {
    return out << "angle: " << a.radians;
}

int main() {
    auto d = Degrees{1};
    auto a1 = Angle{Radians{1.65}};
    auto a2 = Angle{Degrees{240}};
    std::cout << a1 << '\n' << "Press any key to continue...\n";
    std::cin.get();
}

Notice how we have made the constructors for Radians and Degrees explicit. This prevents them from being called implicitely, requiring that the user declare their intent.

Now in this case, implicit construction such as

auto a = Angle{1.65}; // won't compile due to ambiguity

would cause an ambiguity error, but that may not remain true over many iterations of API releases.

For example if we removed or changed the visibility of one of the Angle constructors, the code above would compile without error.

The explicit constructors for Degrees and Radians prevents this hazard and make the code clearer.

We can add user defined literals as syntactic sugar on top of these types to enable a more terse notation.

For example:

constexpr auto operator ""_deg(long double d) { return Degrees(d); }

and

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

enable

auto a = Angle{240.0_deg};

and

auto a = Angle{1.65_rad};

respectively

This works because of the enhanced constexpr facilities in recent versions of the language. Since we marked our constructors as compile time-evaluatable, they can be used as the types of literals. Note my literals are potentially truncating from long double to double and I have yet to learn why declaring them as long double is required at least in MSVC 19.11.25505.

Update:

After reading through the other answers, which I thoroughly enjoyed (thanks all), I realized that much more compile time evaluation was in order so I added a slightly revised implementation incorporating some of what I learned. It is also more lightweight than my previous version and doesn't use accessors.

The implementation above is the revised version.

Upvotes: 3

Kyle Markley
Kyle Markley

Reputation: 185

I'll say at the beginning that this is a heavier approach, but you can make a rigorous separation of degrees and radians throughout your application by using opaque typedefs. This would prevent accidental mixing of values with different units -- they become compile-time errors rather than run-time errors. I have an opaque typedef library that makes it convenient to create numeric opaque typedefs and to express the operations allowed on them.

You could make separate degrees and radians types, with degrees convertible to radians, like this:

#include "opaque/numeric_typedef.hpp"

constexpr auto PI = 22 / 7.0;

struct degrees : opaque::numeric_typedef<double, degrees> {
  using base = opaque::numeric_typedef<double, degrees>;
  using base::base;
};

struct radians : opaque::numeric_typedef<double, radians> {
  using base = opaque::numeric_typedef<double, radians>;
  using base::base;
  constexpr radians(degrees d) : base(d.value/180.0 * PI) { }
};

class Angle {
public:
  Angle(radians r) : angle_in_radians(r) { }
private:
  radians angle_in_radians;
};

int main() {
  Angle a(degrees(90.0));
  Angle b(radians(1.23));
  Angle c(5.0); // compile error
}

Upvotes: 1

Joseph Thomson
Joseph Thomson

Reputation: 10443

One option is to use disambiguation tags.

constexpr struct radians_t {
    constexpr explicit radians_t() = default;
} radians;

constexpr struct degrees_t {
    constexpr explicit degrees_t() = default;
} degrees;

Then define your constructors:

foo(double a, radians_t);
foo(double a, degrees_t);

And invoke like so:

foo(90.0, degrees);
foo(3.14, radians);

While superficially similar to the "named factory" solution, disambiguation tags are compatible with things like perfect forwarding and placement new. For example:

auto p1 = new (buffer) foo(90.0, degrees);
auto p2 = std::make_unique<foo>(3.14, radians);

Upvotes: 1

user0042
user0042

Reputation: 8018

What would be the best practice in C++, to define two separate constructors, one having input in degrees whilst the other in radians?

The best practice would be to have only a single constructor that clearly takes only one of them (most probably radians, since all the trigonometrical functions work with radians).

You should have additional functions to convert the values. One way is to use user defined literals1.

These are meant to give plain values a context with a particular unit to use.


I would go the following way:

constexpr long double operator"" _deg ( long double deg )
{
    return deg2rad(deg);
}

long double deg2rad(long double deg) {
    return deg*3.141592/180;
}

long double rad2deg(long double rad) {
    return (rad/3.141592)*180;
}

class Angle {
public:

    /**
     * Default constructor.
     */
    Angle() : radians() {}
    /**
     * Takes the initial angle value as radians.
     */
    Angle(long double rad) : radians(rad) {}

    // Convenience setters and getters
    void rad(long double value) {
        radians = value;
    }
    long double rad() const {
        return radians;
    }
    void deg(long double value) {
        radians = deg2rad(value);
    }
    long double deg() const {
        return rad2deg(radians);
    }
private:
    long double radians; // Only use radians internally
};

int main() {
    Angle ang1(180_deg); // Initialize using the constexpr conversion
    long double deg = 0;
    std::cout << "Enter an angle value in degrees: " << std::flush;
    std::cin >> deg;
    Angle ang2(deg2rad(deg));

}

1 The examples from the documentation even have one for converting degrees to radians.

Upvotes: 6

Aconcagua
Aconcagua

Reputation: 25536

Varying Benjamin's approach:

class Angle
{
public:
    class Radian; // both with constructor accepting the value
    class Degree; // and a member to store it - ommitting internals

    Angle(Radian const& r);
    Angle(Degree const& d);
};

Angle(Angle::Degree(180));
Angle(Angle::Radian(4.7));

Upvotes: 1

Benjamin Lindley
Benjamin Lindley

Reputation: 103751

Use the named constructor idiom. Make your constructor private, and pick whichever you prefer for the argument type, degrees or radians. Then make appropriately named static member functions which do the necessary conversion.

class Angle {
public:
    static Angle radians(double r) { return Angle(r); }
    static Angle degrees(double d) { return Angle(d / 180.0 * PI); }
private:
    double angle_in_radians;
    Angle(double r) :angle_in_radians(r) {}
};

Upvotes: 15

Related Questions