Introduce chrono in an existing codebase
std::chrono
is part of the standard library since C++11, chances are you already heard of it.
Nevertheless, in bigger projects, with multiple dependencies, it is not uncommon to still rely on time_t
or other integral types to track time.
Using integral types (time_t
included) has two big disadvantages.
The first is, an implicit conversion between integral types; consider the following example
int get_time();
void set_time(unsigned int);
void bar(){
set_time(get_time());
}
While the compiler might generate a warning, in many projects, warnings about implicit conversion between integral types are shut off because there are too many of them.
The solution should be to "fix" them, but writing set_time(static_cast<unsigned int>(get_time()));
is error-prone, and does not really make the code more readable.
The second, and most important issue, is that even when using the same types, different functions might operate on different time intervals; for example
int get_time_in_milliseconds();
void set_time(unsigned int seconds);
void bar(){
set_time(get_time_in_milliseconds());
}
In this case, there are no warnings, and I am not aware of any static analyzer taking millisecond
as part of the function name or variable into account during the analysis.
One possible fix would be to change the code to set_time(get_time()/1000);
, but what if set_time
is changed in order to work with milliseconds (or another time unit)?
std::chrono
offers a fix for both issues, it provides a type-safe set of types for representing a duration.
In particular
-
it does not convert implicitly from types that do not represent a time
-
it provides a type-safe interface for converting between seconds, milliseconds, and other time units
How to integrate it into existing code
So far, so good, but how easy is it to integrate with existing code?
There two main functions for converting between std::chrono
and integral types: constructors and the member function count:
int seconds_as_int = 42;
auto seconds = std::chrono::seconds(seconds_as_int);
auto minutes = std::chrono::minutes(2);
int minutes_as_int = minutes.count();
Conversion between std::chrono
types
It is possible to convert implicitly from std::chrono::minutes
to std::chrono::seconds
but not the opposite.
The conversion from minutes to seconds is lossless (just multiply by 60), while seconds to minutes is lossy, as not every second is mapped to a minute. For this reason, one needs to make an explicit conversion, either with std::chrono::duration_cast
, std::chrono::floor
, std::chrono::round
, or std::chrono::ceil
.
static_assert(
std::chrono::minutes(1) ==
std::chrono::round<std::chrono::minutes>(std::chrono::seconds(50))
);
static_assert(
std::chrono::minutes(0) ==
std::chrono::floor<std::chrono::minutes>(std::chrono::seconds(50))
);
static_assert(
std::chrono::minutes(2) ==
std::chrono::ceil<std::chrono::minutes>(std::chrono::seconds(61))
);
static_assert(
std::chrono::minutes(0) ==
std::chrono::duration_cast<std::chrono::minutes>(std::chrono::seconds(59))
);
std::chrono::duration_cast
is a direct replacement for dividing by 60 (when converting from minutes to seconds) or another quantity, thus it should cover most use cases.
The other conversion functions were added in C++17, but it should not be hard to backport to previous standards if one needs to.
Prefer duration_cast before count than adjusting the unit afterward
Prefer to write std::chrono::duration_cast<std::chrono::seconds>(time).count()
like in
std::chrono::milliseconds get_time();
void set_time(int seconds);
void bar(){
auto time = get_time();
set_time(std::chrono::duration_cast<std::chrono::seconds>(time).count());
}
instead of time.count()/1000
as in
std::chrono::milliseconds get_time();
void set_time(int seconds);
void bar(){
auto time = get_time();
set_time(time.count()/1000);
}
The second form only works as intended if time
is of type std::chrono::milliseconds
, if get_time
changes the return type, the code will still compile but compute the wrong value.
On the other hand std::chrono::duration_cast<std::chrono::seconds>(time)
will convert any std::chrono
type to seconds, thus .count()
will always deliver the amount of what set_time
expects.
Less verbose
While it is nice that std::chrono
offers a more type-safe API, it is verbose.
The verbosity can either be reduced by importing the whole chrono namespace in another, like using namespace c = std::chrono
, or by using namespace literals (since C++14).
#include <chrono>
void foo(std::chrono::milliseconds);
void bar(){
using namespace std::chrono_literals;
auto timeout = 2s; // 2s is equivalent to std::chrono::seconds(2)
foo(timeout);
foo(1min); // 1min is equivalent to std::chrono::minutes(1);
}
One can use the following prefixes
-
h
for hours -
min
for minutes -
s
for seconds -
ms
for milliseconds -
us
for microseconds -
ns
for nanoseconds
Note 📝 | since C++20 there is also d for the day of the month (not for counting days), and y for the current year (not for counting years). |
Arithmetic operators
The std::chrono
types support most if not all arithmetic operators, and when different quantities are mixed together, the implicit conversion operators will do the right thing™, or you’ll get a compiler error
#include <chrono> int main(){ auto h = std::chrono::hours(1); ++h std::chrono::minutes m = h; m++; m = m + h; }
Thus it should be possible to replace an integral type with std::chrono
inside a function or a class and have most of the code working unchanged.
Pay attention to the default constructor
The classes std::chrono::duration
all have a default constructor, but what does it do?
It behave just like the underlying type.
For example, in the case of std::chrono::milliseconds
, which probably uses an "normal" integral type as underlying type, the default constructor will not initialize the underlying type.
Thus
#include <chrono>
#include <iostream>
int main(){
std::chrono::milliseconds ms;
std::cout << ms.count();
}
has undefined behaviors, and might print a very big number (but might also print "hello world", nothing, or crash, …).
I would have personally preferred for std::chrono::duration
to not have a default constructor, in order to make it at least a compile-error.
Conclusion
These are not in-depth notes about std::chrono
and what has been added in C++20 as calendar functionalities. Just a short overview of the important pieces on how the std::chrono
types interact with other entities.
Constructors and the count
member function makes it possible to interact with existing code, which means it is possible to gradually convert an existing codebase from time_t
or other integral types to a more type-safe API.
Do you want to share your opinion? Or is there an error, some parts that are not clear enough?
You can contact me anytime.