I suggest an approach like
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 made the constructors for Radians and Degrees explicit. This prevents them from being invoked implicitly by requiring the user to announce their intention.
Now in this case, an implicit construction such as
auto a = Angle{1.65};
will result in an ambiguity error, but this may not remain true in many iterations of API releases.
For example, if we removed or changed the visibility of one of the Angle constructors, the above code will compile without errors.
The explicit constructors for Degrees and Radians prevent this danger and make the code more understandable.
We can add user-defined literals as syntactic sugar on top of these types to include more precise notations.
For example:
constexpr auto operator ""_deg(long double d) { return Degrees(d); }
and
constexpr auto operator ""_rad(long double r) { return Radians(r); }
turn on
auto a = Angle{240.0_deg};
and
auto a = Angle{1.65_rad};
respectively
This works because of the advanced features of constexpr in the latest versions of the language. Since we designated our constructors as compiled over time, they can be used as literal types. Note that my literals are potentially truncated from long double to double , and I have yet to find out why declaring them as long double is required, at least in MSVC 11/19/25505.
Update:
After reading the other answers that I thoroughly enjoyed (thanks everyone), I realized that a lot more compilation time was fine, so I added a slightly revised version, including some of what I found out. It is also lighter than my previous version, and does not use accessors.
The implementation above is the revised version.