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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With