5
\$\begingroup\$

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), "");
}
\$\endgroup\$
4
  • \$\begingroup\$ We don't help you write new code, but we will be glad to address your other concerns. \$\endgroup\$
    – user34073
    Commented Sep 22, 2015 at 1:42
  • \$\begingroup\$ @Hosch250 It was something as simple as 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\$
    – user85057
    Commented Sep 22, 2015 at 1:45
  • \$\begingroup\$ As long as it works, it is welcome. \$\endgroup\$
    – user34073
    Commented Sep 22, 2015 at 1:45
  • \$\begingroup\$ For a library going down this route, have a look at: github.com/mpusz/units \$\endgroup\$
    – user47787
    Commented Mar 21, 2023 at 16:06

1 Answer 1

5
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.