std::chrono::duration
is a great example of how to create "units" using std::ratio
. std::ratio
by itself is not very useful for a "units" library, and the two choices are either something like Boost.Unit
(which can be very complicated to use) or to write it yourself (why reinvent the wheel?) Therefore I'm trying to write a wrapper around std::chrono::duration
for some very basic types, like meter, feet, inches and centimeters.
Right now I feel like the way I've constructed the types is sloppy. It took me a while to get it right (at least for the test cases.)
I also haven't figured out a good way to "rename" the types either. I've considered creating a templated struct that inherits from duration and then just renames the typedefs.
Another concern I have is whether I should use double
instead of float
and if there's a better way to avoid floating point errors.
#include <iostream>
#include <chrono>
#include <ratio>
#include <cmath> /* for std::abs(double) */
// https://isocpp.org/wiki/faq/newbie#floating-point-arith
// "Why doesn’t my floating-point comparison work?"
constexpr inline bool isEqual(double x, double y)
{
const double epsilon = 1e-5;
return std::abs(x - y) <= epsilon * std::abs(x);
// see Knuth section 4.2.2 pages 217-218
}
int main()
{
using meters = std::chrono::seconds;
// There are 100 centimeters per meter
using centimeters = std::chrono::duration<float, std::ratio_divide<meters::period, std::ratio<100>>>;
// There are 2.54 centimeters per inch
// and 39.3701 inches per meter
using inches = std::chrono::duration<float, std::ratio_divide<centimeters::period, std::ratio<100, 254>>>;
// There are 12 inches per foot
using feet = std::chrono::duration<float, std::ratio_multiply<std::ratio<12>, inches::period>>;
static_assert(inches(1) * 12 == feet(1), "");
static_assert(centimeters(2.54) == inches(1), "");
/* Thanks to rounding, inches(1) / 2.54 is 0.393701.
* To get 0.3937007874, we need to use inches(1 / 2.54)
* instead.
*/
static_assert(inches(1 / 2.54) == centimeters(1), "");
static_assert(centimeters(100) == meters(1), "");
static_assert(inches(centimeters(100)) == meters(1), "");
static_assert(isEqual(inches(centimeters(100)).count(), 39.3701), "");
}
1 Answer 1
Meters are Not Seconds
using meters = std::chrono::seconds;
I know it's obvious, but it bears stating. A meter
is not a second
. You compare a meter and a second, you cannot add them, etc. They are different types completely. So what you want is to make a different type:
template <typename T, class Ratio = std::ratio<1>>
class distance;
using meters = distance<int64_t>;
I'll leave the implementation of distance
up to you. But it should absolutely be a different class than seconds
!
Use the Standard Ratios
<ratio>
actually provides a bunch of standard ratios for your use. You should use them:
using centimeters = distance<int64_t, std::centi>;
You'll note that std::centi
is std::ratio<1, 100>
, which makes more sense for centimeters than std::ratio<100>
.
Use integral types
You'll notice I used int64_t
instead of float
or double
. Prefer using integers whenever possible to simply sidestep the issue of floating point arithmetic altogether.
template <typename Rep, typename Period = std::ratio<1>> struct unit : public std::chrono::duration<Rep, Period> { using ratio = Period; };
I figured it was too "hypothetical" to include in the question. \$\endgroup\$