Przejdź do głównej zawartości

<chrono> introduction


This article will summarize presentation by Howard Hinnant which he gave during CppCon 2016. Howard gives comprehensive walk through c++11 chrono library.



TL;DR (Summary)

C++11 introduced <chrono> library for handling time. Use it instead of <time.h> / <ctime> because it gives you better type safety and precision.
Library provides those concepts:
  • time points - moments in time (as in "noon, December 1st 2000", "2016-01-31 14:15:32")
  • time durations - "length" (amount) of time that passes between two time points ("two hours", "2563 milliseconds")
  • clocks - providers of time points (like wall clock, stopper)
Query clock to get time points; calculate difference between two time points to get time duration. You can use time points/duration with <thread> library for sleep, wait_for/wait_until functions.
Use auto, because otherwise type names are quite verbose.

Time Duration

std::time_duration is a measure of how much time passed. It is measured in seconds or its multiples (here multiples meaning greater as well as smaller amounts).

std::chrono::seconds

Seconds are easiest unit to reason about. They are also basic time unit internally in library, but this is not always apparent.
std::chrono::seconds is a class that stores how much time passed in seconds.
This is trivial type (trivially copy/move/construct/destructible).

Internals

By default internally it is signed long long / int64_t - simple arithmetic type of sizeof() == 8. When doing release build (with optimizations), there is no time penalty for using std::chrono instead of long long.
Despite internal representation, seconds are not implicitly convertible to/from integers.  This is because integers don't store information about unit of time and library uses type safety to protect us against conversion errors.

Construction

seconds s; // gibberish
seconds s{}; // zero-initialization
seconds s{3}; // 3 seconds
//seconds s = 3; ERROR: not implicitly constructible from int

Printing

// cout << s <<endl; // BAD
cout << s.count() << "s" <<endl; // GOOD, explicit cast

Casting

void foo(seconds d) {...}
// f(3); ERROR: not implicitly constructible from int
f (seconds{3}); // using r-value
f (3s); // only C++14

Operations

Arithmetic operations are supported, you can add and subtract seconds ( + - ). Again, there is no implicit conversion from int. Comparison is also supported with operators: >  <  <=  >=  ==  !=

Range

The amount of time passed that can be represented chrono::seconds is limited by range of internal type. The range of allowed values can be checked by using static methods: seconds::min(), seconds::max().
Because by default internal representation is signed, we can get negative time duration. This may seem strange, but think about this example: Race car reached finish line in record time, beating previous champion by 5 seconds. How much time time longer was it riding than record? Minus 5 seconds.

std::chrono::milliseconds and others

Milliseconds type is the same as seconds, only has 1000-times smaller range and 1000-times greater precision. Other duration types follow similar pattern. There are types from nanoseconds to hours, each covers a range of at least ±292 years. You can easily create other duration types.

Implicit conversions

All duration types convert implicitly among themselves as long as conversion is not lossy. So this works:
void foo(milliseconds d) {
    cout << d.count() << "ms" << endl;
}
seconds s{3};
foo(s);
// prints "3000ms"
but this doesn't:
void bar(seconds d) {
    cout << d.count() << "s" << endl;
}
milliseconds s{3001};
// bar(s); ERROR

Comparison and arithmetics

Comparing seconds and milliseconds works using operators <, <=, >, >=, ==, !=. When comparing with const of other type, conversion is free (compile time).
Arithmetic with operators + and - is allowed. "Mixed mode" arithmetic (again, conversions) works and produced result of greater precission:
2s + 3ms == 2003ms;

Casting

Implicit

Implicit casting (using some duration type when other duration type is expected) works when it is safe. Lossless (converting e.g. seconds to milli) always works. Lossy is blocked at compile time to protect against loss of information.
If there is truncation, it will not compile. If using floating point representation (I will explain in a second), conversion always works but is prone to rounding error.
Always prefer implicit conversion. If it works, it is exact and correct.

Explicit

When we are OK with losing precision, we have to explicitly tell this to the compiler. To force the loss use duration_cast<target_type>. duration_cast always truncates toward 0:
seconds p_down = duration_cast<seconds>(3400ms); // 3s
seconds p_up = duration_cast<seconds>(3600ms); // 3s
seconds n_down = duration_cast<seconds>(-3400ms); // -3s
seconds n_up = duration_cast<seconds>(-3600ms); // -3s

For C++1z there are proposed additional truncating functions (to test them I had to install GCC-7, 6.3 was not enough):
  • floor<> toward negative infinity 
  • ceil<> toward positive infinity 
  • round<> toward nearest and towards even on a tie
    // floor goes down (to negative infinity)
    seconds p_floor = floor<seconds>(3600ms); // 3s
    seconds n_floor = floor<seconds>(-3600ms); // -4s

    // ceil goes up
    seconds p_ceil = ceil<seconds>(3600ms); // 4s
    seconds n_ceil = ceil<seconds>(-3600ms); // -3s

    // round goes to closest integer
    seconds p_round4 = round<seconds>(3400ms); // 3s
    seconds p_round6 = round<seconds>(3600ms); // 4s
    seconds n_round4 = round<seconds>(-3400ms); // -3s
    seconds n_round6 = round<seconds>(-3600ms); // -4s

    // but on draw, it goes to even number (not up!)
    seconds p_round5u = round<seconds>(3500ms); // 4s
    seconds p_round5d = round<seconds>(2500ms); // 2s
    seconds n_round5u = round<seconds>(-3500ms); // -4s
    seconds n_round5d = round<seconds>(-2500ms); // -2s
Rounding to even, not up, was surprizing to me. It turns out however, that rounding always up has side effect of shifting average up. By rounding to even, we usually avoid shifting it (see SO answer).

.count()

There is one more way to convert duration types: .count() method. It returns number of ticks in duration in internal representations. This is method of last chance, because it totally forgoes type safety.
However it may be needed when you work with I/O or legacy code. Reverse conversion (long long into duration) is achieved by duration constructor.

Prefer implicit

  1. Always prefer to use implicit conversion.
  2. If it is not possible, use duration_cast<> and others
  3. As last chance, use .count(), but avoid it at all cost

Representation

Storage type (Rep)

I wrote earlier that duration types are in fact signed long long by default. This is customizable by
using seconds32 = std::chrono::duration<int32_t>;
This will work with whole library but operate on 32 bit int instead of 64bit long long (will be cheaper).
We might as well use only unsigned type, or larger type. Only constrain is this must be arithmetic type. Even double/float will work.
using secondsReal = std::chrono::duration<double>;
When using real type, all casts are allowed, even those to less precise durations. After such cast new value will just use fraction part: 3123ms == 3.123s.

Period

Period tells us how long (in real time) is a tick. A tick is unit of time that is counted by Rep. Period is represented by ratio<N,D> (a fraction) relative to 1 second. For example second has ratio<1,1>, millisecond ratio<1,1000> and hour ratio<3600,1>.
Thanks to this feature, you can create custom duration classes that work with any time unit you want. Your class Fortnight with ratio<14*24*3600,1> will easily convert/cast to std::seconds without any additional work.
It is nice to know, that Period is not stored in instances of Duration. Instead it is part of class known only during compilation. Thanks to this, all conversions are calculated at compile time and there is no memory footprint.
If you don't specify the period in duration<> template instantiation, <1,1> will be assumed. So std::duration is measuring seconds by default.

Time points

Time points represents... points in time. When duration means "how much time passed", time point means "when exacly". Think of them as a single read-out of a calendar / wall clock.

Arithmetic

We can add/substract duration to time_point and receive time_point.
We can only substract two time_points and receive duration.
Lastly, we can compare 2 time_points using arithmetic operators <, <=, >, >=, ==, !=.
    auto tp1 = system_clock::now();
    std::this_thread::sleep_for(2s);
    auto tp2 = system_clock::now();
    auto diff = tp2 - tp1;
    assert (diff > 1999ms);
    assert (diff < 1h);
    assert (diff != 2s); // sleep is not perfect
    assert (duration_cast<seconds>(diff) == 2s);

Internal representation

Internally time_point class contains at run time a single duration member. At compile time it also contains Clock that it is based on.
Similarily to duration, you can choose storage type. Then we can implicitly cast to more precise representation or force cast with time_point_cast to coarser representation. Even more dangerous is explicit cast duration to/from time_point with .time_since_epoch(). As last resort use .time_since_epoch().count() (same caveats as for .count() with duration), that returns number of ticks in internal duration.
time_point<system_clock> tp = system_clock::now();  // auto is good, use it.
static_assert(sizeof(tp) == sizeof(long)); 
time_point<system_clock, seconds>tp_sec = time_point_cast<seconds>(tp);
time_point<system_clock, milliseconds> tp_msec = time_point_cast<milliseconds>(tp);
// see why auto is good?

// cout << tp << endl; // does not compile
auto time_passed = tp.time_since_epoch();
// time_passed is of type duration<system_clock::duration>,
// for me duration<nanoseconds>
long ticks = time_passed.count(); 
In example above I avoided using auto to show types explicitly. In production code this would be bad, uncomfortable style.

Clocks

Clock object are source of time_points. Time_points created by one clock interact with other time_points created by this same clock. time_points created by different Clocks are not interoperable, and trying to combine them will cause compile-time error.
There are two Clock classes that you need to know:
  • system_clock is like a calendar / wall clock. Time_points represent time of day and have meaing on their own (realtime property). You need to expect that time may be adjusted when daylight saving changes or user changes time using system settings or ntp-update. You can however expect that comparing time_poinst acquired in separate processes makes some sense (system-wide property).
  • steady_clock is like a stopwatch. Tells us how much time passed since some time in the past (usually since start of the system), but absolute value of time_points is worthless. Those time_point are always good for comparing during the same program run - their difference has real meaning.
There is third clock, high_resolution_clock. In theory it was supposed to be clock with most precise ticks. However it does not give any guarantee. You can't count on it being steady and monotonic like steady_clock and neither is it system-wide nor represents real time. Best thing is to avoid it.
    auto tp_sys = system_clock::now();
    auto tp_inc = steady_clock::now();
    this_thread::sleep_for(5s);
    auto tp_sys2 = system_clock::now();
    auto tp_inc2 = steady_clock::now();

    cout 
    << "system clock before " <<tp_sys.time_since_epoch().count()  <<endl;
    << "system clock after  " <<tp_sys2.time_since_epoch().count() <<endl;
    << "steady clock before " <<tp_inc.time_since_epoch().count()  <<endl;
    << "steady clock after  " <<tp_inc2.time_since_epoch().count() <<endl;
Outputs (seconds part in bold)
system clock before 1494532574013087456
system clock after  1494532579013272202
steady clock before 56987669915614
steady clock after  56992670099734

Casting to string

Sometimes you will want to have textual representation of time. There are several methods for this.

Howard Hinnant date library

Howard is author of CppCon presentation and main author of <chrono> library. https://github.com/HowardHinnant/date introduces logical extensions for <chrono>, among them streaming chrono types to iostream.
#include "date.h"
#include <iostream>

int
main()
{
    using namespace date;
    using namespace std::chrono;
    std::cout << system_clock::now() << " UTC\n";
}
which just outputs for me: 2017-05-11 20:10:07.930671545 UTC. It is possible to alter format of printed string. Also possible is customizing timezone, using header tz.h; otherwise time is printed in UTC, regardless of local zone.
Parts of this library are currently proposed for inclusion into standard.

Conversion to time_t (C way)

time_t is old structure to contain time_points. It has seconds precision, so is not always sufficient.
#include <iostream>
#include <chrono>
#include <ctime> // C Time Library

using namespace std::chrono;

int main()
{
    system_clock::time_point p = system_clock::now();

    std::time_t tt = system_clock::to_time_t(p);
    char *string_rep = std::ctime(&tt);
    std::cout << string_rep << std::endl; // for example : Thu May 11 22:06:24 2017

    // now with custom formating
    //std::tm *tm = gmtime(&tt); // for UTC time representation
    setlocale(LC_TIME, "pl_PL.utf8");
    std::tm *tm = localtime(&tt); // for local time representation

    char buf[70];
    strftime(buf, sizeof buf, "%A %c", tm);
    std::cout << buf << std::endl; // for example : czwartek czw, 11 maj 2017, 22:06:24
}
Other formating options are available here.

Other informations

Limitations

std::chrono library does not handle slipseconds and timezones. Their efffect is visible through system_clock readings, but only if some external mechanism adjusts correctly clock of operating system.

Suffixes

In order to use suffix 's' for std::seconds in C++14, you need
using namespace std::literals::chrono_literals
Note however, that this may cause conflicts with suffix 's' for std::string. For this reason you need to decide which of two literals you want to import in given scope.
All suffixes available in C++14 are ns, us, ms, s, min, h.

See also

Falsehoods programmers believe about time - a reason why you should never write your own time library: you are most likely underqualified.

Komentarze

Popularne posty z tego bloga

GDB - beyond basics

GDB is console debugger that every Linux-using programmer heard about. It is not however easy to learn. Greg Law in his CppCon talk presented some of obscure, but useful features.

Text user interface Normally we use GDB with command line interface (CI). Beyond this, GDB has TUI based on Curses library. To activate it, use keyboard shortcut ctrl-x-a (hold ctrl, press x, unpress x, press a). Now you can see the code as you go through it.
ctrl-x-a - activate/deactivate TUIctrl-l - when screen gets messed up, use it to redraw. Happens when program prints to stdout/stderrctrl-p / ctrl-n - since you can't use arrows to reuse previously written command, use ctr-p/n instead of arrow up/down
ctrl-f / ctrl-n are arrows left / right
ctrl-a / ctrl-e are home / end (all those are copied from Emacs)ctrl-x-2 - second window (assembly). Shell You can run shell commands inside GDB command line. Just use keyword "shell" at the beginning. Examples:
shell psshell cat temporary_file.txtshell k…

C++11 random - introduction

C++11 introduced robust random numbers library, <random>. Its author does great introduction in the video from Cppcon:
I will attempt to create short tutorial/HOWTO for this library.
Bad, old times Before C++11, when you wanted to get random numbers in range {1, 2, ... 10}, you wrote something like this:
#include <stdio.h>      /* printf, NULL */ #include <stdlib.h>     /* srand, rand */ #include <time.h>       /* time */ int main () { srand (time(NULL)); int num1 = rand()%10 + 1; int num2 = rand()%10 + 1; return 0; } Several things are wrong here:
It always gives uniform distribution (1 is as probable as 5). This is usually fine, but when not, you need a lot of additional work.Actually, it is not very uniform.If you want for some reason to reseed the generator (call srand(time(NULL)) again) shortly after first seeding, you may get repeated answersYou only get discreet numbers (integers)Random algorithm is not specified, so results are not portab…