[ edit: changed meters/yards to foo/bar; this isn't about converting meters to yards. ]
What's the best way to attach a type to a scalar such as a double
? The typical use-case is units-of-measure (but I'm not looking for an actual implementation, boost has one).
This would appear to be a simple as:
template <typename T>
struct Double final
{
typedef T type;
double value;
};
namespace tags
{
struct foo final {};
struct bar final {};
}
constexpr double FOOS_TO_BARS_ = 3.141592654;
inline Double<tags::bar> to_bars(const Double<tags::foo>& foos)
{
return Double<tags::bar> { foos.value * FOOS_TO_BARS_ };
}
static void test(double value)
{
using namespace tags;
const Double<foo> value_in_foos{ value };
const Double<bar> value_in_bars = to_bars(value_in_foos);
}
Is that really the case? Or are there hidden complexities or other important considerations to this approach?
This would seem far, far superior to
inline double foos_to_bars(double foos)
{
return foos * FOOS_TO_BARS_;
}
without adding hardly any complexity or overhead.
Firstly, yes, I think the way you have suggested is quite reasonable, though whether it is to be preferred would depend on the context. Your approach has the advantage that you define conversions that might not just be simple multiplications (example Celsius and Fahrenheit).
Your method however does create different types, which leads to a need to create conversions, this can be good or bad depending on the use.
(I appreciate that your yards and metres were just an example, I'll use it as an just as an example too)
If I'm writing code that deals with lengths, (most of) the logic is going to be the same whatever the units. Whilst I could make the function that contains that logic a template so it can take different units, there's still a reasonable use case where data is needed from 2 different sources and is supplied in to different units. In this situation I'd rather be dealing in one Length class rather than a class per unit, these lengths could either hold their conversion information or it could just use one fixed unit with conversion being done at the input/output stages.
On the other hand when we have different types for different measurements e.g. length, area, temperature. Not having default conversions between these types is a good thing. And it's good that I can't accidently add a length to a temperature.
(Of course multiplication of types is different.)
I'd go with a ratio-based approach, much like std::chrono
. (Howard Hinnant shows it in his recent C++Con 2016 talk about <chrono>
)
template<typename Ratio = std::ratio<1>, typename T = double>
struct Distance
{
using ratio = Ratio;
T value;
};
template<typename To, typename From>
To distance_cast(From f)
{
using r = std::ratio_divide<typename To::ratio, typename From::ratio>;
return To{ f.value * r::den / r::num };
}
using yard = Distance<std::ratio<10936133,10000000>>;
using meter = Distance<>;
using kilometer = Distance<std::kilo>;
using foot = Distance<std::ratio<3048,10000>>;
This is a naive implementation and probably could be improved a lot (at the very least by allowing implicit casts where they're safe), but it's a proof of concept and it's trivially extensible.
Pros:
meter m = yard{10}
is either a compile time error or a safe implicit conversion,- pretty type names, you'd have to work against the solution very hard to make an invalid conversion
- simple to use
Cons:
- Possible integer overflows/precision problems (may be alleviated by quality of implementation?)
- may be non-trivial to implement correctly
In my opinion, your approach is over-designed to the point that bugs have crept in that are hard to spot. Even at this point the syntactic complexity you have introduced has allowed your conversion to become inaccurate: you are out from the 8th decimal significant figure.
The standard conversion is 1 inch is 25.4mm which means that one yard is exactly 0.9144m.
Neither this nor its reciprocal can be represented exactly in IEEE754 binary floating point.
If I were you I'd define
constexpr double METERS_IN_YARDS = 0.9144;
constexpr double YARDS_IN_METERS = 1.0 / 0.9144;
to keep the bugs away, and work in double precision floating point arithmetic the old-fashioned way.
来源:https://stackoverflow.com/questions/40381918/preferred-mechanism-to-attach-a-type-to-a-scalar