I was always interested in how computers can deal with the messiness of the human way of counting time: months of different lengths, 60 minutes is an hour, 24 hours in a day, it’s logic, isn’t it?
My fascination for the subject was met by my disgust (I really struggled to find a less harsh word, I failed) for the tools C++ provided us with, which where, before C++11, not particularly good (and this is an euphemism). To be blunt, C++ had no tools to speak of, it had the C header <ctime>, and in that header we find this masterpiece to convert epoch (seconds from 1/1/1970) to date:
struct tm *localtime( const time_t *time );
The inattentive reader might think that this is a C API that returns an heap allocated pointer: so, let’s wrap it up in a std::unique_ptr with custom deleter or a wrapper class and be done with it, no? No! From the documentation:
The result is stored in static storage and a pointer to that static storage is returned.
[…]
This function
From: https://en.cppreference.com/w/c/chrono/localtimelocaltimemay not be thread-safe.
Reading this paragraph made me lose a couple of years of life. So, before C++11 dealing with time was complex, potentially non thread-safe and not particularly nice. There are extensions that avoid the lack of thread-safety, localtime_r for Posix, localtime_s for Windows (not to be confused with the C11 function of the same name) and other similarly named functions.
The rest of the articles in this series will summarize some videos from Howard Hinnant who, after giving us move semantics, threads, mutexes and std::unique_ptr gave us also <chrono> and continues to work to provide date and timezone support in C++20 .
The videos will be presented in logical order, not in chronological order (ironic, given the subject at hand).
We will start with “A <chrono> tutorial – CppCon 2016”.
Durations (C++11/14/17)
In the above video we are introduced to the goodies of the <chrono> header in its C++14 incarnation (with a small detour to C++17). The mantra that is repeated during the presentation is: “If it compiles, it works”, meaning that the type system is designed to enforce as many checks as possible at compile time.
Key features
- The library provides and allows the definition of duration types (e.g. hours, seconds, etc.);
- Library-provided durations behave like signed built-in integral types, i.e. they can be declared without being initialized. So, please, make sure you initialize them.
- Implicit conversions (and mixed operations, like sum) between built-in types (
int,float) and duration types are not possible, appropriate constructors must be called, this makes sense: if I write2in my code, what do I mean? Seconds, hours, bananas? Let’s assume that implicit conversions are possible: if we havea + 2the value of time expressed by2depends ona. Isaseconds? Then,2is seconds. We refactor andais millisecond, now2is milliseconds. Refactoring becomes a nightmare; - The duration classes take care of converting types, for example you can compare or sum seconds and milliseconds, the correct transformations will be done for you;
- Lossless (no truncation error) conversions are implicit (e.g. from seconds to milliseconds), potentially lossy conversions (e.g. milliseconds to seconds) requires explicit casts:
duration_cast<target_type>in C++11, and alsofloor,ceilandroundin C++17; - The use of the member function
count()to extract the number of units (ticks) stored in a duration is discouraged and should be used only for I/O and interaction with legacy code.
Why all these constraints? Because using, for example, int for seconds and relying on variable names or comments to make sure that the user of your library provides the right units is brittle. Furthermore, refactoring a codebase that relies on these conventions is a nightmare.
Below a small code snippet to summarize what we saw so far:
#include <chrono>
#include <iostream>
//Works: implicit lossless conversion from s to ms
std::chrono::milliseconds sum1(std::chrono::seconds m1, std::chrono::milliseconds m2) {
//here m1 * 1000 done by the type system
return m1 + m2;
}
//Does not work, no implicit lossy conversion from ms to s
std::chrono::seconds sum2(std::chrono::seconds m1, std::chrono::milliseconds m2) {
return m1 + m2;
}
//Does not work, no implicit conversion from built-in types
std::chrono::seconds sum3(std::chrono::seconds m1, int m2) {
return m1 + m2;
}
int main()
{
std::chrono::seconds t1{ 4 };
std::chrono::seconds t2{ 4 };
std::chrono::microseconds s = sum1(t1, t2);
//Last resort, count() to extract the time in ms
std::cout << s.count() << " ms\n";
}
Lifting the hood
Like any C++ feature, expert developers can lift the hood of the machinery provided by <chrono> and fiddle with it. std::chrono::seconds and other durations are alias for
template<
class Rep,
class Period = std::ratio<1>
> class duration;
Where Rep is the representation, i.e. the underlying type that is storing the duration (for example int). Rep can also be floating point. In this case all the conversions are considered lossless (no truncation error, only rounding errors) and therefore implicit.
Period is way more interesting and gives us an idea of how the type system works: it is a fraction representing the duration in seconds, so a millisecond has Period = std::ratio<1, 1000>, while a minute has Period = std::ratio<60, 1>. One can create its own duration, say a floating-point half-day made of 12 hours (see below).
std::ratio is a core building block of the compile-time machinery that makes <chrono> so performing. When summing two durations with different ratios, the least common denominator will be calculated at compile-time, leaving only a couple of multiplications and a sum to be performed at run-time. Below a simple example where we sum a custom floating point duration spanning 12 hours and a 8 hours expressed using a built-in type. The output is, as expected, 20 hours.
#include <chrono>
#include <iostream>
int main() {
auto constexpr secondsIn12Hours = 43200;
using halfDay = std::chrono::duration<float, std::ratio<secondsIn12Hours, 1>>;
halfDay workedTime{ 1 };
std::chrono::hours sleepedIn{ 8 };
std::chrono::hours totalTime = std::chrono::duration_cast<std::chrono::hours>(workedTime + sleepedIn);
std::cout << "Total time used " << totalTime.count() << '\n';
}
We have analyzed duration: basically we can handle the concept of “how long” in C++. But what about the concept of a time in point, like “now”? clock and time point to the rescue.
Clocks (C++11/14/17)
The clocks defined <chrono> allow us to get the current time thanks to the static function now(). A clock can be steady (i.e. the tick is constant and it will never be adjusted) or not. A clock counts a number of ticks (e.g. seconds) from a point in time called the epoch.
std::chrono::steady_clock: this clock is steady and should be used as a stopwatch, to measure intervals;std::chrono::system_clock: this represents the wall-clock of the system. Usually (mandatory from C++20) it uses as epoch (starting point) 00:00 1/1/1970 UTC.std::chrono::high_resolution_clock: clock with the smallest tick, usually an alias for one of the two previous ones;
Time Points (C++11/14/17)
A clock and a duration, together, define a time_point: the clock defines the epoch (i.e. instant zero) and the Duration expresses how many ticks are elapsed from the epoch.
template<
class Clock,
class Duration = typename Clock::duration
> class time_point;
We can cast one time_point that uses seconds as ticks to a time_point that uses hours with time_point_cast (C++11) or floor, ceil and round (C++17). The duration from the epoch can be retrieved thanks to the member function time_since_epoch().
Timepoints and Durations have consistent algebra, enforced by the time system: two timepoints can be subtracted to obtain a duration, but not summed. A duration can be added to a timepoint.
int main() {
std::chrono::steady_clock::time_point t1 = std::chrono::steady_clock::now();
std::this_thread::sleep_until(t1 + std::chrono::seconds{ 2 });
std::chrono::steady_clock::time_point t2 = std::chrono::steady_clock::now();
std::chrono::steady_clock::duration duration = t2 - t1;
//sum 2 time points does not work!
//auto does_not_work = t1 + t2;
auto works = duration + std::chrono::steady_clock::now();
//We need cast because we don't know, in general, the duration type of the clock
std::cout << "Time elapsed " <<
std::chrono::duration_cast<std::chrono::milliseconds>(duration).count()
<< " ms\n";
//Two equivalent ways of casting
//Cast on duration
std::chrono::system_clock::time_point sysTime = std::chrono::system_clock::now();
std::cout << "Hours elapsed from 1970 " <<
std::chrono::duration_cast<std::chrono::hours>(sysTime.time_since_epoch()).count()
<< '\n';
//Cast the time point
auto sysTime2 = std::chrono::time_point_cast<std::chrono::hours>(std::chrono::system_clock::now());
std::cout << "Hours elapsed from 1970 " <<
sysTime2.time_since_epoch().count()
<< '\n';
}
Takeaways
- The wrapper classes have (with the appropriate optimization switch enabled) the same performance of the built-in types;
- The typesystem takes care of the of the conversions;
- Using
count()to retrieve the number of ticks should be done as last resort, only for I/O or compatibility with legacy APIs; - The type system is easy to expand and user-defined durations are first-class citizens.
Next time we will peek at the future, we will analyse how we will be able, using the same type system, to handle days, months and years.