Howard Hinnant Howard Hinnant - 10 days ago 6
C++ Question

What is the best way to form custom std::chrono::durations and std::ratios?

I was reading this excellent answer which used the comical time duration unit

microfortnights
to illustrate a good point in a memorable way.

typedef std::ratio<756, 625> microfortnights;
std::chrono::duration<int, microfortnights> two_weeks(1000000);


And the question occurred to me:


If I really wanted to do this (more likely some other non-trivial
duration such as the time available during a frame, or during N cycles
of a processor), what is the best way to do this?


I know
ratio<N, D>
will create a unique type associated with each value of
N
and
D
. So
ratio<4, 6>
is a different type than
ratio<2, 3>
, even though they represent the same (reduced) fraction. Do I always have to do the math to simplify the conversion factor to reduced terms?

It would be more convenient to write:

using microfortnights = std::chrono::duration<long, ratio<86400*14, 1000000>>;


instead of:

using microfortnights = std::chrono::duration<long, ratio<756, 625>>;


But then these would be two different types, instead of the same type. The first expression is easier to inspect for correctness. But there are many representations of this fraction, and the second is arguably canonical, and thus preferred. If I have too many types wandering around my program which actually represent the same unit, then that may lead to unnecessary template code bloat.

Answer

Below I'm ignoring namespaces in the interest of being concise. duration is in namespace std::chrono, and ratio is in namespace std.

There are two good ways to always ensure that your ratio is reduced to lowest terms without having to do the arithmetic yourself. The first is quite direct:

The direct formulation

If you just want to jump straight to microfortnights, but without having to figure out that the reduced fraction of 86,400*14/1,000,000 is 756/625, just add ::type after the ratio:

using microfortnights = duration<long, ratio<86400*14, 1000000>::type>;

The nested type of every ratio<N, D> is another ratio<Nr, Dr> where Nr/Dr is the reduced fraction N/D. If N/D is already reduced, then ratio<N, D>::type is the same type as ratio<N, D>. Indeed, had I already figured out that 756/625 was the correct reduced fraction, but was just paranoid in thinking it might could be reduced further, I could have written:

using microfortnights = duration<long, ratio<756, 625>::type>;

So if you have any doubt that your ratio is expressed in lowest terms, or just don't want to be bothered with checking, you can always append the ::type to your ratio type just to be sure.

The verbose formulation

Custom time duration units often pop up as part of a family. And it is often convenient to have the entire family available to your code. For example microfortnights is obviously related to fortnights, which in turn is related to weeks, which is derived from days, which is derived from hours (or from seconds if you prefer).

By building up your family one unit at a time, you not only make the entire family available, you also reduce the chance of errors by relating one family member to another with the simplest possible conversion. Additionally, making use of std::ratio_multiply and std::ratio_divide, instead of multiplying literals also means you don't have to keep insert ::type everywhere to ensure that you keep your ratio in lowest terms.

For example:

using days = duration<long, ratio_multiply<hours::period, ratio<24>>>;

ratio_multiply is a typedef-name to the result of the multiplication already reduced to lowest terms. So the above is the exact same type as:

using days = duration<long, ratio<86400>>;

You can even have both definitions in the same translation unit, and you will not get a re-definition error. In any event you can now say:

using weeks           = duration<long, ratio_multiply<days::period,       ratio<7>>>;
using fortnights      = duration<long, ratio_multiply<weeks::period,      ratio<2>>>;
using microfortnights = duration<long, ratio_multiply<fortnights::period, micro>>;

And we have ended up with a typedef-name for microfortnights that is the exact same type as in our direct formulation, but through a series of much simpler conversions. We still do not have to be bothered with reducing fractions to lowest terms, and we now have several useful units instead of just one.

Also note the use of std::micro in place of std::ratio<1, 1000000>. This is another place to avoid careless errors. It is so easy (at least for me) to mistype (and misread) the number of zeroes.

Comments