Skip to main content

La blog

Chrono on Steroids

Many new exciting features made it into C++20. Who has never heard of Modules or Concepts, or even Ranges and the spaceship operator? This article however takes a look at another part, namely the chrono library and the upcoming calendar and time zone support.

Voted into the working draft in 2018, the P0355R7 proposal by Howard Hinnant and Tomasz Kamiński aims to extend the date and time utilities bundled in the chrono library with calendar and time zone support.

What do we get?

A lot. Seriously, take a look at the proposal or read the already available documentation on cppreference. Everything you can do with tm and time_t from the old C API you can now do with convenient C++ types and functions.

The new features come with many promises (quoted from the proposal):

  • Seamless integration with the existing <chrono> library.
  • Type safety.
  • Detection of errors at compile time.
  • Performance.
  • Ease of use.
  • Readable code.
  • No artificial restrictions on precision. Note: current financial software is currently in the middle of a transition from seconds precision to milliseconds precision. This library handles that transition as seamlessly as <chrono> does.

Pre C++20

Before we dig into the new fancy code let’s take a look at how we had to do things before.

Current time

Following code will print the current time:

1
2
3
auto now = std::chrono::system_clock::now();
auto now_t = std::chrono::system_clock::to_time_t(now);
std::cout << std::ctime(&now_t) << std::endl;
Sat Jul 18 15:36:25 2020

So we first use the C++ API in <chrono> header with system_clock and then convert back to a time_t to process it via ctime in <ctime> header. Not only do we needlessly switch API but we also are not thread-safe with ctime.

Let’s try to stick to one API:

1
2
auto now_t = std::time(nullptr);
std::cout << std::ctime(&now_t) << std::endl;
Sat Jul 18 15:36:25 2020

Ok that is cleaner but we are back to the C API and still aren’t thread-safe.

Thread-safe:

1
2
3
auto now_t = std::time(nullptr);
char buf[25];
std::cout << ctime_r(&now_t, buf);
Sat Jul 18 15:36:25 2020

Now we are thread-safe but we are also in C land (depending on your system headers you might need to use ctime_s).

Current time but custom format

How do we display a different format, let’s say I also want to see milliseconds and have a numerical date display.

The numerical display can be achieved with put_time from <iomanip> and its many format specifiers.

1
2
auto now_t = std::time(nullptr);
std::cout << std::put_time(std::localtime(&now_t), "%F %T") << std::endl;
2020-07-18 15:36:25

Thread-safe:

1
2
3
auto now_t = std::time(nullptr);
tm buf;
std::cout << std::put_time(localtime_r(&now_t, &buf), "%F %T") << std::endl;
2020-07-18 15:36:25

Depending on your system headers you might need to use localtime_s.

However showing anything more precise than seconds is impossible with the tm struct (and respectively time_t holding seconds since 1 January 1970). That means we have to fallback to std::chrono::system_clock or clock_gettime which use the system clock and generally have a higher precision.

1
2
3
4
5
6
timespec ts;
if (!clock_gettime(CLOCK_REALTIME, &ts)) {
    tm buf;
    std::cout << std::put_time(localtime_r(&ts.tv_sec, &buf), "%F %T.")
              << ts.tv_nsec/1000000 << std::endl;
}
2020-07-18 15:36:25.368

Can we get this with chrono? Well kinda, but we are back to a bunch of conversions back and forth.

1
2
3
4
5
6
7
auto now = std::chrono::system_clock::now();
auto now_t = std::chrono::system_clock::to_time_t(now);
auto duration = now.time_since_epoch();
auto mills = std::chrono::duration_cast<std::chrono::milliseconds>(duration);
tm buf;
std::cout << std::put_time(localtime_r(&now_t, &buf), "%F %T.")
          << mills.count()%1000 << std::endl;
2020-07-18 15:36:25.368

Specific time zone

Now let’s try to show the time in Québec, Canada which follows the Eastern Time Zone (ET) (UTC -5/-4).

1
2
3
4
setenv("TZ", "EST5EDT", 1);
auto now_t = std::time(nullptr);
char buf[25];
std::cout << ctime_r(&now_t, buf);
Sat Jul 18 09:36:25 2020

As I’m located in Germany (UTC +1/+2) the time is now six hours earlier.

Set to specific time

Let’s assume we want to create a time point 18 July 2020, 15:36:25 in CEST time zone.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
tm now {
    .tm_sec   = 25,
    .tm_min   = 36,
    .tm_hour  = 15,
    .tm_mday  = 18,
    .tm_mon   = 6,
    .tm_year  = 2020 - 1900,
    .tm_wday  = 6,
    .tm_yday  = 199,
    .tm_isdst = 1
};
std::cout << std::put_time(&now, "%F %T") << std::endl;
2020-07-18 15:36:25

I’m using designated initializers just so it is possible to see what all the fields are for. The field tm_yday I had to calculate. It describes the zero-initialized day count in the current year.

Let’s see how it looks like when we parse the time point from a string:

1
2
3
4
tm now;
std::istringstream ss("15:36:25 18.07.2020");
ss >> std::get_time(&now, "%T %d.%m.%Y");
std::cout << std::put_time(&now, "%F %T") << std::endl;
2020-07-18 15:36:25

Summary

Now it’s not the biggest deal to use the C API but the usage examples above show time management is definitely not a first class citizen in C++.

New stuff

Let’s try all the above examples with the new C++ 20 functionality. As I write this article neither libstd++ nor libc++ have implemented it yet, so I will use the reference implementation HowardHinnant/date.

Current time (C++ 20)

For the following to work with the reference implementation, specifically to use the operator<< the line using namespace date; has to be added in the beginning.

1
std::cout << std::chrono::system_clock::now() << std::endl;
2020-07-18 13:36:25.368151566

What we see here is the system clock precision (nanoseconds on my system) and the time is displayed in UTC (my system timezone is CEST = UTC +2).

Let’s show the local time. Note that with the reference implementation you have to include the date namespace for the streaming operator and use zoned_time and current_zone from the reference implementation (instead of std:: namespace use date:: namespace for those).

1
2
3
std::cout << std::chrono::zoned_time(std::chrono::current_zone(),
                                     std::chrono::system_clock::now())
          << std::endl;
2020-07-18 15:36:25.368151566 CEST

Current time but custom format (C++ 20)

Now let’s format the output so it just displays the date and the time with millisecond precision with help of the <format> header. Again with the reference implementation we have to use the date namespace for the streaming operator and use floor, zoned_time, current_zone and format from there (the <format> header is not shipped with my stdlibc++ version yet).

1
2
3
4
auto now = std::chrono::system_clock::now()
auto mills = std::chrono::floor<std::chrono::milliseconds>(now);
auto local = std::chrono::zoned_time(std::chrono::current_zone(), mills);
std::cout << std::format("%F %T", local) << std::endl;
2020-07-18 15:36:25.368

Specific time zone (C++ 20)

Selecting a specific time zone is quite straightforward. We just take the same command we took for the local time and change the time zone description. With the reference implementation we have to include the date namespace for the streaming operator and also use the zoned_time function from there.

1
2
3
std::cout << std::zoned_time("EST5EDT",
                             std::chrono::system_clock::now())
          << std::endl;
2020-07-18 09:36:25.368151566 EDT

Set to specific time (C++ 20)

Following code sets the current time. With the reference implementation we have to enable the date namespace again for the streaming operator and we also have to use sys_days from there.

1
2
3
using namespace std::chrono_literals;
auto now = std::chrono::sys_days(July/18/2020) + 15h + 36min + 25s;
std::cout << now << std::endl;
2020-07-18 15:36:25

Not only is this very nicely readable but it’s also typesafe. The fancy operator/ construction of a date can be used in all sensible orderings, e.g. 2020y/July/18, 18d/July/2020, … Keep also in mind that the construction is very efficient as the compiler will calculate the result at compile time in this case.

Let’s take a look at how we parse a time point. Again the date namespace is required when using the reference implementation as well as the parse function.

1
2
3
4
std::istringstream ss("15:36:25 18.07.2020");
std::chrono::system_clock::time_point now;
ss >> std::chrono::parse("%T %d.%m.%Y", now);
std::cout << now << std::endl;
2020-07-18 15:36:25.000000000

Conclusion

The new chrono features simplify working with times, dates and time zones tremendously. It is no longer required to work with the C API. That results in type safety when working with time and dates. Additionally time conversions are covered by the chrono API and manual conversions mistakes are no longer an issue.

Chrono uses the C API under the hood and a recent compiler will optimize to the exact same assembler a human would get when programming with C.1

Apart from the aspects shown in this article there are many more new features like custom calendar support, calendar conversions, indexed weekdays (e.g. 2nd Monday of a month), last weekday of a month and am/pm hour format among others.


  1. date – date reference documentation by Howard Hinnant ↩︎