2021-01-03

15: How to Optimally Handle Date and Time in C++

<The previous article in this series | The table of contents of this series | The next article in this series>

What container should be used? 'time_t' and 'tm' still? Or '::std::chrono::time_point'? The characteristics of each datum type should be known.

Topics


About: C++

The table of contents of this article


Starting Context


  • The reader has a basic knowledge on C++.

Target Context


  • The reader will know the characteristics of each date and time datum type, in order to be able to judge how to optimally handle date and time in C++ for each situation.

Orientation


The C++ standard 17 is used here.

No extra library is used here.


Main Body


1: The 'time_t' and 'tm' Pair



1-1: The Characteristics of 'time_t' and 'tm'


Hypothesizer 7
'time_t' and 'tm' are some old datum types that originated in C.

In the C++ standard, 'time_t' is not specified so concretely, but just as seconds from an epoch.

When is the epoch? . . . That depends on the compiler, although that is probably '1970-01-01T00:00:00 UTC'.

Well, without the epoch being specified, how can I know what date and time a 'time_t' instance represents? . . . I have to get a 'tm' (which is a struct that has the date and time separated in some components (like the year, the month, etc.)) instance from the 'time_t' instance via a function like 'localtime', which secretly knows the epoch, unless I dare to be compiler dependent. . . . So, I cannot do without 'tm' unless I am interested in only durations between 'time_t' instances.

On the other hand, cannot I do with only 'tm', without 'time_t'? . . . Well, a problem is that 'tm' is not necessarily expedient for calculation on the datum: for example, adding '42' days to the datum. Besides, 'time_t' is more efficient in the datum size.

Let me check the datum sizes of 'time_t' and 'tm', like this.

@C++ Source Code
				cout << "###### checking the sizes of 'time_t' and 'tm' Start" << endl << flush;
				{
					time_t l_dateAndTimeInTime_t = 0;
					tm l_dateAndTimeInTm;
					cout << "######### the sizes of 'time_t' and 'tm': " << sizeof (l_dateAndTimeInTime_t) << ", " << sizeof (l_dateAndTimeInTm) << endl << flush;
				}
				cout << "###### checking the sizes of 'time_t' and 'tm' End" << endl << flush;

The output with GCC 9.3.0 in Linux (in fact, WSL 2)

@Output
###### checking the sizes of 'time_t' and 'tm' Start
######### the sizes of 'time_t' and 'tm': 8, 56
###### checking the sizes of 'time_t' and 'tm' End

The output with Visual C++ 2019 in Microsoft Windows 10

@Output
###### checking the sizes of 'time_t' and 'tm' Start
######### the sizes of 'time_t' and 'tm': 8, 36
###### checking the sizes of 'time_t' and 'tm' End

That difference may be important if many date and time data have to be stashed in the memory.

So, 'time_t' and 'tm' are a pair that is meant to be used together, basically.

While the resolution of 'tm' is obviously 1 second, what is the resolution of 'time_t'? Does "seconds" mean 'seconds in an integer'? . . . Let me test that like this.

@C++ Source Code
				cout << "###### checking the resolution of 'time_t' Start" << endl << flush;
				{
					time_t l_epochInTime_t = 0;
					time_t l_possiblySlightlyAdvancedDateAndTimeFromEpochInTime_t = 0.1;
					cout << "######### 'time_t' has a resolution of sub-second: " << ((l_epochInTime_t == l_possiblySlightlyAdvancedDateAndTimeFromEpochInTime_t) ? "false": "true") << endl << flush;
				}
				cout << "###### checking the resolution of 'time_t' End" << endl << flush;

The output with GCC 9.3.0 in Linux (in fact, WSL 2) or with Visual C++ 2019 in Microsoft Windows 10

@Output
###### checking the resolution of 'time_t' Start
######### 'time_t' has a resolution of sub-second: false
###### checking the resolution of 'time_t' End

The code can be compiled (although the compiler may warn about trying to put the fraction into the 'time_t' variable), but the fraction part is ignored anyway. . . . So, the resolution is 1 second, at least in those compilers.


1-2: Some Typical Usage


Hypothesizer 7
This is some typical usage of 'time_t' and 'tm'.

@C++ Source Code
				cout << "###### some typical usage of the 'time_t' and 'tm' pair Start" << endl << flush;
				{
					time_t l_42DaysAfterNowInTime_t = 0;
					time (&l_42DaysAfterNowInTime_t);
					l_42DaysAfterNowInTime_t += 60 * 60 * 24 * 42;
					tm l_42DaysAfterNowLocalTimeInTm = *(localtime (&l_42DaysAfterNowInTime_t));
					cout << "######### the 42 days after now local time: " << put_time (&l_42DaysAfterNowLocalTimeInTm, "%Y-%m-%dT%H:%M:%S") << endl << flush;
					tm l_42YearsAnd42DaysAfterNowUtcTimeInTm = *(gmtime (&l_42DaysAfterNowInTime_t));
					cout << "######### the 42 days after now UTC time: " << put_time (&l_42YearsAnd42DaysAfterNowUtcTimeInTm, "%Y-%m-%dT%H:%M:%S") << endl << flush;
					l_42YearsAnd42DaysAfterNowUtcTimeInTm.tm_year += 42;
					// 'tm_mon' and 'tm_mday' may have to be adjusted if they are '2' and '29', but spare me here, please.
					cout << "######### the 42 years and 42 days after now UTC time: " << put_time (&l_42YearsAnd42DaysAfterNowUtcTimeInTm, "%Y-%m-%dT%H:%M:%S") << endl << flush;
					// the 'tm' instance is supposed to be in the local time for 'mktime', but let me correct the datum after the conversion, because adjusting the 'tm' instance is tedious.
					time_t l_42YearsAnd42DaysAfterNowDateAndTimeInTime_t = mktime (&l_42YearsAnd42DaysAfterNowUtcTimeInTm);
					time_t l_epochInTime_t (0);
					l_42YearsAnd42DaysAfterNowDateAndTimeInTime_t += 60 * 60 * (localtime (&l_epochInTime_t))->tm_hour;
					cout << "######### the 42 years and 42 days after now reconstructed local time: " << put_time (localtime (&l_42YearsAnd42DaysAfterNowDateAndTimeInTime_t), "%Y-%m-%dT%H:%M:%S") << endl << flush;
					
				}
				cout << "###### some typical usage of the 'time_t' and 'tm' pair End" << endl << flush;

The output with GCC 9.3.0 in Linux (in fact, WSL 2) or with Visual C++ 2019 in Microsoft Windows 10

@Output
###### some typical usage of the 'time_t' and 'tm' pair Start
######### the 42 days after now local time: 2021-02-10T21:42:54
######### the 42 days after now UTC time: 2021-02-10T12:42:54
######### the 42 years and 42 days after now UTC time: 2063-02-10T12:42:54
######### the 42 years and 42 days after now reconstructed local time: 2063-02-10T21:42:54
###### some typical usage of the 'time_t' and 'tm' pair End

The system date and time can be gotten as a 'time_t' instance; 'time_t' is expedient for some calculation like adding or subtracting seconds, minutes, hours, or days, but may not be so for adding or subtracting months or years (because the number of the days of a month depends on the month, etc.), for which 'tm' may be better (although the judgment of leap years is still required); dulations can be gotten easily with 'time_t'; any 'time_t' instance can be converted to a 'tm' instance and vice versa.


2: '::std::chrono::time_point'



2-1: The Motivation for '::std::chrono::time_point'


Hypothesizer 7
'::std::chrono::time_point' is a class template that takes a 'Clock' and a 'Duration' as type parameters.

What is "Clock"? Any 'Clock' is basically an epoch, a 'Duration', and a 'now ()' method.

What is "Duration"? Any 'Duration' is basically a resolution.

Well, why does 'time_point' take a 'Duration' while 'Clock' has already a 'Duration'? The 'Duration' of 'Clock' determines the resolution of the 'now ()' method, while the 'Duration' of 'time_point' determines the resolution of 'time_point' as a datum container, but the 'Duration' of 'time_point' can be (and is usually) omitted to default to be the same with the 'Duration' of the 'Clock'.

While we already have the 'time_t' and 'tm' pair, why do we need '::std::chrono::time_point'?

The motivation does not seem to be having a handy object-oriented datum type that replaces the crude 'time_t' and 'tm' pair, but be having higher and controlled resolutions.

Generally speaking, 'time_point' is for getting durations between 'time_point' instances, rather than for pointing to a specific date and time in the calendar: basically, only 'time_point <system_clock>' lets us know the specific date and time in the calendar, and even that is by just shuffling off the responsibility onto the 'time_t' and 'tm' pair.

Be aware that having a resolution C++-wise does not necessarily mean that there is really that resolution. For example, 'now ()' of 1 nanosecond resolution returns an instance of 'time_point' of 1 nanosecond resolution, but if the system clock has a lower resolution OS-wise, of course, the time point contained in the 'time_point' instance will not have that precision.


2-2: Some 'Clock's


Hypothesizer 7
There are 3 major 'Clock's: '::std::chrono::system_clock', '::std::chrono::steady_clock', and '::std::chrono::high_resolution_clock'.

'steady_clock' is a steady 'Clock', which means that the latter of any 2 'now ()' calls never returns any older time point.

Well, is that special? What clock would be otherwise?

In fact, 'system_clock' may be otherwise, because the system clock can be adjusted backward by NTP, hand, or whatever.

'system_clock' is a 'Clock' tied to the system clock.

Well, is any 'Clock' not tied to the system clock? No, because being tied to the system clock would cause the clock to move backward, as I said.

'high_resolution_clock' is a 'Clock' that is guaranteed to be of the highest resolution. It may be or not be steady. In fact, 'high_resolution_clock' is an alias of 'system_clock' in GCC 9.3.0 and an alias of 'steady_clock' in Visual C++ 2019,

Anyway, what are the resolutions of the 3 clocks? . . . That depends on the compiler, but in my GCC 9.3.0, the resolutions of 'system_clock' and 'steady_clock' are 1 nanosecond and 1 nanosecond; in my Visual C++ 2019, the resolutions of 'system_clock' and 'steady_clock' are 100 nanoseconds and 1 nanosecond, as can be checked with this code.

@C++ Source Code
				cout << "###### checking the resolution of 'system_clock' Start" << endl << flush;
				{
					cout << "######### the tick of 'system_clock': " << system_clock::period::num << " / " << system_clock::period::den << endl << flush;
					time_point <system_clock> l_nowInTime_pointOfSystem_clock (system_clock::now ());
					nanoseconds l_durationOfNowFromEpochInNanoseconds = l_nowInTime_pointOfSystem_clock.time_since_epoch ();
					cout << "######### the duration of now from the epoch in nanoseconds: " << l_durationOfNowFromEpochInNanoseconds.count () << endl << flush;
					time_point <system_clock> l_1NanosecondFromNowInTime_pointOfSystem_clock (duration_cast <system_clock::duration> (l_durationOfNowFromEpochInNanoseconds + nanoseconds (1)));
					cout << "######### the resolution of 'system_clock' is 1 nanosecond: " << ((l_1NanosecondFromNowInTime_pointOfSystem_clock == l_nowInTime_pointOfSystem_clock) ? "false": "true") << endl << flush;
				}
				cout << "###### checking the resolution of 'system_clock' End" << endl << flush;
				
				cout << "###### checking the resolution of 'steady_clock' Start" << endl << flush;
				{
					cout << "######### the tick of 'steady_clock': " << steady_clock::period::num << " / " << steady_clock::period::den << endl << flush;
					time_point <steady_clock> l_nowInTime_pointOfSteady_clock (steady_clock::now ());
					nanoseconds l_durationOfNowFromEpochInNanoseconds = l_nowInTime_pointOfSteady_clock.time_since_epoch ();
					cout << "######### the duration of now from the epoch in nanoseconds: " << l_durationOfNowFromEpochInNanoseconds.count () << endl << flush;
					time_point <steady_clock> l_1NanosecondFromNowInTime_pointOfSteady_clock (duration_cast <steady_clock::duration> (l_durationOfNowFromEpochInNanoseconds + nanoseconds (1)));
					cout << "######### the resolution of 'steady_clock' is 1 nanosecond: " << ((l_1NanosecondFromNowInTime_pointOfSteady_clock == l_nowInTime_pointOfSteady_clock) ? "false": "true") << endl << flush;
				}
				cout << "######### checking the resolution of 'steady_clock' End" << endl << flush;
				cout << "###### checking the resolution of 'system_clock' End" << endl << flush;

The output with GCC 9.3.0 in Linux (in fact, WSL 2)

@Output
###### checking the resolution of 'system_clock' Start
######### the tick of 'system_clock': 1 / 1000000000
######### the duration of now from the epoch in nanoseconds: 1609332174715479100
######### the resolution of 'system_clock' is 1 nanosecond: true
###### checking the resolution of 'system_clock' End
###### checking the resolution of 'steady_clock' Start
######### the tick of 'steady_clock': 1 / 1000000000
######### the duration of now from the epoch in nanoseconds: 179608188792790
######### the resolution of 'steady_clock' is 1 nanosecond: true
######### checking the resolution of 'steady_clock' End
###### checking the resolution of 'system_clock' End

The output with Visual C++ 2019 in Microsoft Windows 10

@Output
###### checking the resolution of 'system_clock' Start
######### the tick of 'system_clock': 1 / 10000000
######### the duration of now from the epoch in nanoseconds: 1609332389157981200
######### the resolution of 'system_clock' is 1 nanosecond: false
###### checking the resolution of 'system_clock' End
###### checking the resolution of 'steady_clock' Start
######### the tick of 'steady_clock': 1 / 1000000000
######### the duration of now from the epoch in nanoseconds: 1528852745499300
######### the resolution of 'steady_clock' is 1 nanosecond: true
######### checking the resolution of 'steady_clock' End
###### checking the resolution of 'system_clock' End

The time point resolution of Microsoft Windows 10 (at least of my computer) seems to be 100 nanoseconds; so there is no more precision than that with the 'now ()' method of any 'Clock'.

Then, when are the epochs? Well, they depend on the compiler, which is OK, but regrettably, there does not seem to be even any way to know them (at least accurately), generally speaking (the epoch of 'system_clock' can be known though), which means that 'steady_clock' is for only getting durations between 'time_point' instances of the same clock, basically.


2-3: The Size of 'time_point'


Hypothesizer 7
Let me check the size of 'time_point', like this.

@C++ Source Code
				cout << "###### checking the size of 'time_point' Start" << endl << flush;
				{
					time_point <system_clock> l_dateAndTimeInTime_pointOfSystem_clock;
					time_point <steady_clock> l_dateAndTimeInTime_pointOfSteady_clock;
					cout << "######### the sizes of 'time_point <system_clock>' and 'time_point <steady_clock>': " << sizeof (l_dateAndTimeInTime_pointOfSystem_clock) << ", " << sizeof (l_dateAndTimeInTime_pointOfSteady_clock) << endl << flush;
				}
				cout << "###### checking the size of 'time_point' End" << endl << flush;

The output with GCC 9.3.0 in Linux (in fact, WSL 2) or with Visual C++ 2019 in Microsoft Windows 10

@Output
###### checking the size of 'time_point' Start
######### the sizes of 'time_point <system_clock>' and 'time_point <steady_clock>': 8, 8
###### checking the size of 'time_point' End

Oh, it is better than I expected.


2-4: Getting the Calendar Date and Time of Any 'time_point <system_clock>' Instance


Hypothesizer 7
The calendar date and time of any 'time_point <system_clock>' instance can be gotten via 'system_clock::to_time_t (const time_point & t)'.


2-5: Getting the Sub-Second Part of the Calendar Date and Time


Hypothesizer 7
But that gets the calendar date and time only at the 1 second precision. In fact, I want a sub-second precision. Typically speaking, I want to print a 'time_point <system_clock>' instance in a form like '2020-12-29T11:42:36.859428932'.

Supposing that the 'system_clock' epoch is at a flat second of any date and time (I mean, '1975-02-03T13:45:39'is good while '1975-02-03T13:45:39.8' is not), I can get it like this.

@C++ Source Code
				cout << "###### getting the sub-second part of a 'time_point <system_clock>' instance Start" << endl << flush;
				{
					time_point <system_clock> l_nowInTime_pointOfSystem_clock (system_clock::now ());
					time_t const l_nowInTime_t (system_clock::to_time_t (l_nowInTime_pointOfSystem_clock));
					system_clock::duration l_durationOfNowFromEpochInSystem_clockDuration = l_nowInTime_pointOfSystem_clock.time_since_epoch ();
					int l_fractionInNanoseconds (floor <nanoseconds> (l_durationOfNowFromEpochInSystem_clockDuration).count () - floor <seconds> (l_durationOfNowFromEpochInSystem_clockDuration).count () * 1000000000);
					cout << "######### now: " << put_time (localtime (&l_nowInTime_t), "%Y-%m-%dT%H:%M:%S.") << setfill ('0') << setw (9) << l_fractionInNanoseconds << endl << flush;
				}
				cout << "###### getting the sub-second part of a 'time_point <system_clock>' instance End" << endl << flush;


2-6: Daring to Convert Any 'time_point <steady_clock>' Instance to a 'time_point <system_clock>' Instance


Hypothesizer 7
The calendar date and time of any 'time_point' instance of 'steady_clock' cannot be gotten (basically), because the epoch cannot be known, which also means that any 'time_point' instance of 'steady_clock' cannot be converted to a 'time_point' instance of 'system_clock'.

Note that I am not supposing the availability of the C++ standard 20, which has 'template <class Dest, class Source, class Duration> clock_cast (const std::chrono::time_point <Source, Duration> &amp; t)', which I cannot test because my environments do not support the standard level at least officially.

Nevertheless, the difference of the epoch of 'steady_clock' from the epoch of 'system_clock' can be gotten approximately, like this.

@C++ Source Code
					microseconds l_approximatedEpochDifferenceFromSystem_clockToSteady_clock (duration_cast <microseconds> (system_clock::now ().time_since_epoch ()) - duration_cast <microseconds> (steady_clock::now ().time_since_epoch ()));
					cout << "######### the approximated epoch difference from 'system_clock' to 'steady_clock' in microseconds: " << l_approximatedEpochDifferenceFromSystem_clockToSteady_clock.count () << endl << flush;

As the 2 time points are approximately the same, the difference of the durations is approximately the difference of the epochs. As the epoch of 'system_clock' can be gotten, the epoch of 'steady_clock' can be gotten approximately.

Well, what will the error be like? That depends on the platform, of course, but let me test on my platform (which is very slow), like this.

@C++ Source Code
					nanoseconds l_estimatedEpochDifferenceError (duration_cast <nanoseconds> (steady_clock::now ().time_since_epoch ()) - duration_cast <nanoseconds> (steady_clock::now ().time_since_epoch ()));
					cout << "######### the estimated epoch difference error in nanoseconds: " << l_estimatedEpochDifferenceError.count () << endl << flush;

An output with Visual C++ 2019 in Microsoft Windows 10

@Output
######### the estimated epoch difference error in nanoseconds: 200

Well, I seem to be able to expect 1 microsecond precision.

After all, I can convert any 'time_point' instance of 'steady_clock' to a 'time_point' instance of 'system_clock', like this.

@C++ Source Code
				cout << "### daring to convert a 'time_point <steady_clock>' instance to a 'time_point <system_clock>' instance Start" << endl << flush;
				{
					microseconds l_approximatedEpochDifferenceFromSystem_clockToSteady_clock (duration_cast <microseconds> (system_clock::now ().time_since_epoch ()) - duration_cast <microseconds> (steady_clock::now ().time_since_epoch ()));
					time_point <steady_clock> l_nowInTime_pointOfSteady_clock (steady_clock::now ());
					time_point <system_clock> l_nowInTime_pointOfSystem_clock (l_approximatedEpochDifferenceFromSystem_clockToSteady_clock + duration_cast <microseconds> (l_nowInTime_pointOfSteady_clock.time_since_epoch ()));
				}
				cout << "###### daring to convert a 'time_point <steady_clock>' instance  to a 'time_point <system_clock>' instance End" << endl << flush;


2-7: Some Typical Usage


Hypothesizer 7
This is some typical usage of 'time_point'.

@C++ Source Code
				cout << "###### some typical usage of 'time_point <system_clock>' Start" << endl << flush;
				{
					time_point <system_clock> l_42DaysAfterNowInTime_pointOfSystem_clock (system_clock::now ());
					l_42DaysAfterNowInTime_pointOfSystem_clock += hours (24 * 42);
					time_t const l_42DaysAfterNowInTime_t (system_clock::to_time_t (l_42DaysAfterNowInTime_pointOfSystem_clock));
					cout << "######### the 42 days after now: " << put_time (localtime (&l_42DaysAfterNowInTime_t), "%Y-%m-%dT%H:%M:%S") << endl << flush;
				}
				cout << "###### some typical usage of 'time_point <system_clock>' End" << endl << flush;
				
				cout << "###### some typical usage of 'time_point <steady_clock>' Start" << endl << flush;
				{
					time_point <steady_clock> l_dueDateAndTimeInTime_pointOfSteady_clock (steady_clock::now ());
					l_dueDateAndTimeInTime_pointOfSteady_clock += hours (24 * 42);
					cout << "######### the duration of the due date and time from now in seconds: " << (duration_cast <seconds> ( (l_dueDateAndTimeInTime_pointOfSteady_clock - steady_clock::now ())).count ()) << endl << flush;
				}


3: So, What Should I Do?


Hypothesizer 7
So, what should I do?

If a resolution is a requirement, I have no choice, but to use a datum type that has the resolution.

In that sense, the 'time_t' and 'tm' pair is not so versatile, and specifications-wise, 'time_point <high_resolution_clock>' should be the most dependable.

But, 'high_resolution_clock' is a problem in that the steadyness is not clear.

So, my standard is that if steadiness is required, I will use 'time_point <steady_clock>', and otherwise, I will use 'time_point <system_clock>'.

Any 'time_point <steady_clock>' instance can be converted to a 'time_point <system_clock>' instance, if 1 microsecond precision is OK.

The 'time_t' and 'tm' pair is needed in order to get the calendar date time from any 'time_point <system_clock>' instance, anyway.


4: Why Is There Not Any Full-Fledged Object-Oriented Standard Date and Time Type?


Hypothesizer 7
I wonder why the C++ standard will not have any full-fledged object-oriented date and time type.

'time_point' is something that has focused on handling durations than a full-fledged date and time type, as I have seen.

In fact, why can the epoch not be gotten from any 'time_point' instance? Why will any 'time_point' instance not directly offer the calendar date and time?

If someone claims that that is because of an efficiency consideration, I am saying that offering a handy class, which, of course, does not have to be used when an efficiency consideration makes it inappropriate, will be no harm.


References


<The previous article in this series | The table of contents of this series | The next article in this series>