I see std::chrono
being rewritten in various projects. It is better to learn and understand its API than constantly learn new time APIs. The library is broken up into clocks which provide time points, and durations.
There are basically two types of clocks: steady and system. steady_clock
is monotonic. system_clock
usually represents the wall clock time.
Use steady_clock
to measure durations. It is also appropriate for performance analysis on certain operations.
Use system_clock
to relate to a time or date for people. It is also useful for distributed systems. When using this clock’s time, the programmer must consider that it may go backwards.
The now()
method returns a time_point
. The time_point
type is templated with the clock’s type so that one clock’s time_point
will not interoperate by default with another’s. time_point
’s store the duration from a specified epoch. Note that system_time
often uses Unix Time, but the epoch is unspecified.
system_clock
also includes to and from time_t
methods for C compatibility. Below is a reference table with similar C POSIX calls on some systems.
Clock | C++ Name | Monotonic | Usually Wall Time | POSIX |
---|---|---|---|---|
Steady | std::chrono:: steady_clock |
Yes | No | clock_gettime( CLOCK_MONOTONIC, &tp) |
System | std::chrono:: system_clock |
No | Yes | clock_gettime( CLOCK_REALTIME, &tp) |
A std::chrono:::duration
represents the difference between two time points. It consists of two parts: a representation and a period. The representation is the underlying type. For example, it could be a struct
, int
, or double
. The value stored in this type is referred to as ticks. The period is the ratio from those ticks to seconds. Using seconds makes sense, because it is an SI unit.
Below is an example of using the clocks to create a duration.
const auto before = std::chrono::steady_clock::now();
foo();
const auto delta = std::chrono::steady_clock::now() - before;
const double seconds = std::chrono::duration<double>(delta).count();
std::cout << "foo took " << seconds << "s" << std::endl;
Note that steady_clock
was used for durations, because the duration would always be positive. The -
operator returned a duration. Then, std::chrono::duration<double>
converted that representation to a duration with a double representation in seconds. count()
converts from duration<double, std::ratio<1>>
to the underlying type, which is just double
. Double was used because iostream has an overload for it already.
C++14 introduced chrono_literals
. These allow a convenient way to type a literal as a duration.
using namespace std::chrono_literals;
const auto nanos = 10ns;
const auto micros = 10us;
Durations can be used to offset time points as well. See the code below.
const std::chrono::system_clock::time_point t1(1s);
const std::chrono::system_clock::time_point t2(t1 + 10ms);
const auto seconds = std::chrono::duration<double>(t2.time_since_epoch())
std::cout << "time_since_epoch = " << seconds.count() << std::end;
Here a time_point is constructed manually. Then, it is offset by 10 milliseconds to create a new time_point. Then, the time since the epoch is used to get the duration back out. Next, it is converted to seconds. Finally, it is converted to a double for printing. Note that system_clock
is used, because the time since epoch value is usually relatable to other systems.
The various predefined durations are all signed integers with some guarantee on their minimum bit length. Each predefined duration type covers +/- 292 years. That means going from a seconds to nanoseconds will not truncate, but the other way might.
const auto nanos = 100ns;
const auto seconds = std::chrono::duration_cast<std::chrono::seconds>(nanos);
const auto millis = 100ms;
const std::chrono::milliseconds micros = millis;
In the code above, duration_cast
is used so that the compiler knows that the programmer accounted for the risk of truncation. Therefore, going from milliseconds to microseconds did not require a cast.
With chrono, conversion math is unnecessary. See the manual operations done below.
const int64_t s_0 = 100;
const int64_t n_0 = s_0 * static_cast<int64_t>(1e9);
const int64_t n_1 = 1234567891011;
const int64_t s_1 = n_1 / static_cast<int64_t>(1e9);
Instead, prefer to use the chrono duration types and the appropriate casting.
const auto s_0 = 100s;
const std::chrono::nanoseconds n_0 = s_0;
const auto n_1 = 1234567891011ns;
const auto s_1 = std::chrono::duration_cast<std::chrono::seconds>(n_1);
The multiplication and division are built into the type converter. Also, remember that duration_cast
was only needed from nanoseconds to seconds, because of integer truncation.
Both integers and floating point types have limitations on the numbers that they can represent. Integers are straightforward, because they can only represent what is in their range. Their range is dependent on the number of bits. If arithmetic exceeds the limit, they overflow.
Floating point is more complicated because its resolution is dependent on its mantissa and the operation performed. Large floating point numbers can overwhelm the smaller number. For example, on some platforms 2e8f + 1.0f == 2e8f
is true.
Below is a table comparing the common representations and periods. For integer types, the seconds, days, and years are the time starting from 0 until a tick causes integer overflow. For floating point types, the seconds, days, and years are the time until the precision for a single tick is lost. The bits column represents the part of the representation that effectively stores the ticks. This table assumes std::numeric_limits
Representation | Period | Tick Duration | Bits | Seconds | Days | Years |
---|---|---|---|---|---|---|
int32_t | std::ratio<1, 1000> | milliseconds | 31 | 2e6 | 25 | 0.07 |
int32_t | std::ratio<1, 1> | seconds | 31 | 2e9 | 2e4 | 68 |
int64_t | std::ratio<1, 1000000000> | nanoseconds | 63 | 9e9 | 1e5 | 292 |
float | std:ratio<1, 1> | seconds | 24 | 2e7 | 194 | 0.5 |
double | std::ratio<1, 1000000000> | nanoseconds | 52 | 5e6 | 52 | 0.1 |
For certain requirements floating point time representations may be acceptable. A lot of devices are not designed to run for long periods of time. Floating point is easy to use, but is more complicated to account for error. If operations are accumulated, the error may accumulate over time as well. Also, some of the bits may go unused if the whole range isn’t used. This waste is common when storing SI units. Chrono makes fixed point arithmetic easier to use. For these reasons, it’s best to consider using chrono’s integer based durations by default.
The chrono library follows a lot of good design principles. It clears up a lot of confusion by putting more logic in the type and conversion system. Chrono is not a general purpose unit library. It will only handle 0 and 1 exponent units of time. Learning it will lead to good practices.
Source for Post