I’ll be honest with you right off the bat, I hold a particular grudge for std::chrono
. In fact, it’s safe to say one of my favorite hobbies over the past few years has been ranting about std::chrono
to whomever will listen. And so, assuming I was wrong about the library, I’ve forced myself to use it every single time I need to work with duration values for the past year or so. I know the library was written by highly talented individuals, so I believed it had to have some sort of reasoning behind it. I am happy to say I’ve reached enlightenment.
Since std::chrono
’s release, I haven’t seen it used in any serious way in the code-bases I’ve worked on. You see the occasional usage for things like sleep, or maybe a small counter, but nothing integral to the architecture or apis. It’s surprising to me since the game and 3d industry use time everywhere. For animations, which happen over time, for simulations, ai, acceleration and movement, etc. The list goes on and on. I’ll take a wild guess and assume I’m not the only developer scratching his head trying to figure out how to use chrono appropriately and intuitively.
Today, I will share 1 simple rule of thumb which changed my opinion completely and made me fall in love with std::chrono
. Be advised this post contains strong language. Lets illustrate the pitfalls of chrono with some examples. We don’t need complicated examples since it falls on its face within seconds of use.
What does the following print?
std::chrono::minutes m{ 0 };
while (m.count() < 1) {
m += std::chrono::seconds{ 1 };
}
printf("minutes : %dm\n", m.count());
Trick question, it doesn’t compile. The reason is because std::chrono
’s helper types are shit. How often do you have to add seconds to minutes? In my industry, often. When working as a gameplay programmer, many times per day. Yet C++ has a time library that doesn’t allow the most basic time manipulation task… Fret not, there is a solution! Here it is.
What does the following print?
std::chrono::minutes m{ 0 };
while (m.count() < 1) {
m += std ::chrono::duration_cast<std::chrono::minutes>(
std::chrono::seconds{ 1 });
}
printf("minutes : %dm\n", m.count());
Aaaand nothing, again. This example results in an infinite loop. Cute. Since std::chrono::seconds
and std::chrono::minutes
use ints as their underlying type, they are automatically truncated when added together. You are basically writing the following.
int m = 0;
while (m < 1) {
m += 1 / 60;
}
printf("minutes : %dm\n", m);
1 / 60 is equal to 0, and you have an infinite loop. Except it is well hidden behind the verbose chrono apis, making it more difficult to spot during code reviews. What about divisions? Nope. Applying percentages to time? Are your crazy!
Honestly, I don’t even have a good simple solution for this example when using the default, as shipped, std::chrono
. So lets jump right into my proposed technique.
Rule Number One : You Shall Follow Rule Number One
Never, ever, ever use std::chrono
’s helper types and literals. The following types and their corresponding literals should be banned from your code base.
std::chrono::milliseconds
std::chrono::seconds
std::chrono::minutes
std::chrono::hours
std::chrono::days
std::chrono::weeks
std::chrono::months
std::chrono::years
Erase them from your memory. They do not exist. These are not the types you are looking for.
Instead, in every project that needs to use std::chrono
, define the following aliases.
using dmilliseconds = std::chrono::duration<double, std::milli>;
using dseconds = std::chrono::duration<double>;
using dminutes = std::chrono::duration<double, std::ratio<60>>;
using dhours = std::chrono::duration<double, std::ratio<3600>>;
using ddays = std::chrono::duration<double, std::ratio<86400>>;
using dweeks = std::chrono::duration<double, std::ratio<604800>>;
using dmonths = std::chrono::duration<double, std::ratio<2629746>>;
using dyears = std::chrono::duration<double, std::ratio<31556952>>;
// or
using fmilliseconds = std::chrono::duration<float, std::milli>;
using fseconds = std::chrono::duration<float>;
using fminutes = std::chrono::duration<float, std::ratio<60>>;
using fhours = std::chrono::duration<float, std::ratio<3600>>;
using fdays = std::chrono::duration<float, std::ratio<86400>>;
using fweeks = std::chrono::duration<float, std::ratio<604800>>;
using fmonths = std::chrono::duration<float, std::ratio<2629746>>;
using fyears = std::chrono::duration<float, std::ratio<31556952>>;
And that’s it really… Not so climactic in the end. Using either doubles or floats for time durations fixes all the previously mentioned issues and more. Lets implement the problematic example using our new types.
dminutes m{ 0 };
while (m.count() < 1) {
m += dseconds{ 1 };
}
printf("minutes : %fm\n", m.count());
Not only does this compile, and we don’t have to use duration_cast
, but it prints 1 minute as expected.
Changing time durations to floating point types is game-changing. You can convert from milliseconds to hours back to seconds without ever worrying about precision loss or conversion bugs. You can call functions that accept dhours
using dminutes
and they will work as expected. Division, percentages and any other operations you need to perform on time are simple and very robust. The power of chrono becomes apparent.
There are certainly use-cases for using ints as time duration values, but they are few and far between. Usually one would static_cast
count()
at the point of consumption. This guarantees your computations are high precision. If you need a precise integral “tick”, you can create an int alias just as we did with our double durations. At the point of consumption, one would duration_cast
to the tick type. All your time computations can be carried out naturally using floating point durations.
I will reiterate the statement I made earlier. Chrono’s helper types are garbage, but the chrono library itself is absolutely amazing. I don’t know why ints where chosen as the default duration types. I’m sure there was a good reason, maybe historical, but it is what it is.
Ultimately, the standard committee was wrong to choose ints for std::chrono helper types and literals.
Enjoy chrono for humans.
Update I’ve removed microseconds and milliseconds as they may cause precision problems in some edge-cases.