Our software often needs to deal with dates, times and durations. This lesson gives an brief introduction to the the most useful utilities within C++ to deal with these requirements. In C++, the chrono
library is the main way we implement these features.
We can #include
the library, and access its functionality through the std::chrono
namespace.
#include <chrono>
using namespace std::chrono;
In this lesson, we will cover 3 aspects of chrono
- durations, clocks and time points
A duration is a period of time. The chrono
library gives us a range of functions to express durations, such as std::chrono::weeks
, std::chrono::seconds
and more.
#include <chrono>
using namespace std::chrono;
int main() {
duration Duration1 { weeks(2) };
duration Duration2 { hours(5) };
duration Duration3 { milliseconds(100) };
}
Where it makes sense, we can apply arithmetic and comparison operators to durations:
// Add durations together
duration Duration { weeks(2) + days(4) };
// Increase an existing duration
Duration += days(2);
// Doubling a duration
Duration *= 2;
// Comparing durations
if (Duration > days(100)) {
// Duration is longer than 100 days
}
Duration literals for hours and below are available:
#include <chrono>
using namespace std::chrono;
int main() {
duration A { 3h };
duration B { 2min };
duration C { 1h + 2min + 3s + 4ms + 5us + 6ns };
}
These literals require an appropriate using namespace
statement to be available, such as using namespace std::chrono
count
The underlying numeric value within a duration is available using the count
method:
#include <chrono>
#include <iostream>
using namespace std::chrono;
int main() {
duration Duration { 3h };
std::cout << Duration.count() << " Hours" << std::endl;
}
3 Hours
duration_cast
The duration_cast
function allows us to convert a duration into an equivalent one, but using a different unit of measurement. For example, here we convert 3 hours to 180 minutes:
#include <chrono>
#include <iostream>
using namespace std::chrono;
int main() {
duration Duration { 3h };
duration InMinutes { duration_cast<minutes>(Duration) };
std::cout << InMinutes.count() << " Minutes" << std::endl;
}
180 Minutes
sleep_for
In combination with the <thread>
library, we can use durations to cause our application to go to sleep for a period of time:
#include <iostream>
#include <chrono>
#include <thread>
using namespace std;
using namespace std::chrono;
int main() {
cout << "Starting" << endl;
this_thread::sleep_for(seconds(5));
cout << "Done!";
}
A clock is a source of time information. By default, chrono offers 3 clocks:
system_clock
, which uses data from the operating system's built in clock. For most cases, using this is the simplest approach.steady_clock
, which is designed to not be impacted by changes to the system clock. Use this if it is important that users cannot exploit our software by changing their system clock.high_resolution_clock
, which is designed to be highly accurate. Use this if we require millisecond, microsecond or nanosecond level accuracy. This can be useful when measuring performance, for example.Using data from a clock, we can generate a value representing a specific point in time. For example, to get the current time point from the system clock, we can do this:
#include <chrono>
using namespace std::chrono;
int main() {
time_point CurrentTime {
system_clock::now()
};
}
We can also add or remove durations to time points:
time_point ThreeWeeksAgo {
system_clock::now() - weeks(3)
};
And we can generate durations by comparing two time points:
#include <chrono>
#include <thread>
#include <iostream>
using namespace std;
using namespace std::chrono;
int main() {
time_point StartTime {
system_clock::now()
};
this_thread::sleep_for(seconds(5));
time_point EndTime {
system_clock::now()
};
// Create a duration from two timepoints
duration RunningTime {
EndTime - StartTime
};
if (RunningTime >= 5s) {
cout << "That took a while!";
}
}
That took a while!
Often, we will want to see the actual times being calculated within our software. The time_point
objects are not designed for visual output, so there are a few steps we need to go through to get user friendly output.
First, we need to convert our time to a time_t
object. This will convert our time point to a numeric value, usually representing the number of seconds that have passed since midnight on 1st January 1970. This format is commonly called epoch time.
When we're using the system_clock
, a static function system_clock::to_time_t
is available:
time_point StartTime {
system_clock::now()
};
time_t TimeSinceEpoch {
system_clock::to_time_t(StartTime)
};
cout << TimeSinceEpoch;
This will output a number, similar to this:
1651075702
This is enough to let us confirm our time is working, but it's not the most user friendly output. We want something like 28th April 2023
To get there, first we can use the localtime_r
function. This function takes two arguments. The first is a pointer to our time_t
. The second argument is a pointer to a tm
.
tm
is a type of struct designed to hold date and time values, so we will need to create that object:
tm TimeContainer;
Now that we have our time_t
(which we called TimeSinceEpoch
) and our tm
(which we called TimeContainer
), we pass their pointers into the localtime_r
function.
The localtime_r
function will then fill our TimeContainer
with the correct data:
time_point StartTime {
system_clock::now()
};
time_t TimeSinceEpoch {
system_clock::to_time_t(StartTime)
};
tm TimeContainer;
localtime_r(&TimeSinceEpoch, &TimeContainer);
If we inspect our TimeContainer
after running this code, we will find it has properties like tm_year
, tm_month
, tm_hour
and more. These properties will be populated with appropriate values calculated from our TimeSinceEpoch
variable.
We could use this this to generate our desired output, but the standard library provides a std::put_time
function that can help us.
To access it, we need to #include <iomanip>
. We can then call std::put_time
, passing in a pointer to our time container, and a string that describes how we want our time to be formatted.
#include <chrono>
#include <iostream>
#include <iomanip>
using namespace std;
using namespace std::chrono;
int main() {
time_point StartTime {
system_clock::now()
};
time_t TimeSinceEpoch {
system_clock::to_time_t(StartTime)
};
tm TimeContainer;
localtime_r(&TimeSinceEpoch, &TimeContainer);
cout << put_time(
&TimeContainer,
"The current time is %r on %A"
);
}
Running this code, we'd see we finally get a user friendly time output:
The current time is 17:08:22 on Wednesday
The put_time
function examines our format string, and replaces tokens like %r
and %A
with appropriate content from our TimeContainer
.
There are dozens of different tokens we can use to get the output we want. Our options are listed on the official documentation.
Lets put all these concepts together, to see a more complicated example of working with clocks, time points, durations and thread sleeping:
#include <chrono>
#include <iomanip>
#include <iostream>
#include <thread>
using namespace std;
using namespace std::chrono;
void PrintTime(const time_t& Time) {
tm TimeContainer;
localtime_r(&Time, &TimeContainer);
cout << put_time(
&TimeContainer,
"The current time is %r on %A"
) << endl;
}
int main() {
time_point StartTime{system_clock::now()};
PrintTime(system_clock::to_time_t(StartTime));
this_thread::sleep_for(seconds(5));
time_point EndTime{system_clock::now()};
PrintTime(system_clock::to_time_t(EndTime));
duration<float> RunningTime{EndTime - StartTime};
cout << "The running time was "
<< RunningTime.count()
<< " seconds";
}
Our output will be something like:
The current time is 17:23:21 on Wednesday
The current time is 17:23:26 on Wednesday
The running time was 5.01718s
There is something to bear in mind when using sleep_for
, and almost any other way of causing our application to sleep.
That is, our applications will always sleep for slightly longer than we requested. In the previous run, we slept for approximately 0.017 seconds longer than intended.
This is simply due to a nuance in how operating systems schedule processes. We should consider it an instruction to "sleep for at least this amount of time".
In most use cases, these fractions of a second don't matter - however, in some scenarios, they do. For example, if we were creating a clock that updated every second, and updates are delayed by 0.01 seconds on average, our clock would run slow, losing 14 minutes per day.
For these scenarios, we need to compensate for the drift. A common way of doing this is to compare the current time point and the time point from the previous iteration of the loop.
For example, if we find the two time points are 1.01
seconds apart, our next sleep_for
call should only be 0.99 seconds
. This causes our program to self-correct, and remain accurate even when running for long periods of time.
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way