Dates, Times and Durations

Learn the basics of the chrono library and discover how to effectively manage durations, clocks, and time points
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
3D art showing a sundial in a fantasy environment
Ryan McCombe
Ryan McCombe
Updated

Our software often needs to deal with dates, times, and durations. This lesson gives a brief introduction to 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

Chrono Durations

A std::chrono::duration is a period of time, such as two weeks, one hour, or 1 minute and 30 seconds. The chrono library gives us a range of functions to express durations, such as std::chrono::weeks, std::chrono::seconds, and more:

#include <chrono>

int main() {
  using namespace std::chrono;
  duration Duration1 { weeks(2) };
  duration Duration2 { hours(5) };
  duration Duration3 { milliseconds(100) };
}

The duration type has overloaded the arithmetic operators such as + and *=, allowing us to combine durations in intuitive ways:

#include <chrono>

int main() {
  using namespace std::chrono;

  // Add durations together
  duration Duration { weeks(2) + days(4) };

  // Increase an existing duration
  Duration += days(2);

  // Doubling an existing duration
  Duration *= 2;
}

We can also compare durations using comparison operators such as > and <=:

#include <chrono>

int main() {
  using namespace std::chrono;

  duration Duration { weeks(2) + days(4) };

  // This will be true
  if (Duration > days(15)) {
    // ...
  }
}

Duration Literals

Duration literals are available. These require us to have an appropriate using namespace statement in place. Options include:

  • using namespace std::chrono;
  • using namespace std::chrono_literals;
  • using namespace std::literals;

In scopes where an appropriate using statement is in effect, we can then express durations using literals such as h, min and s:

#include <chrono>

int main() {
  using namespace std::chrono;
  duration Hours{5h};
  duration Minutes{5min};
  duration Seconds{5s};
  duration Milliseconds{5ms};
  duration Microseconds{5us};
  duration Nanoseconds{5ns};
}

Duration count()

The underlying numeric value within a duration is available using the count method:

#include <chrono>
#include <iostream>

int main() {
  using namespace std::chrono;
  duration Duration{5h};
  std::cout << Duration.count() << " Hours";
}
5 Hours

Changing units using duration_cast

The duration_cast function allows us to convert a duration into an equivalent duration, but using a different unit of measurement.

In the following example, we convert 1.5 hours to minutes, which results in a count() of 90 as we’d expect:

#include <chrono>
#include <iostream>

int main() {
  using namespace std::chrono;
  duration Duration{1.5h};
  duration Minutes{
    duration_cast<minutes>(Duration)};

  std::cout << Minutes.count() << " Minutes";
}
90 Minutes

Using a duration to Pause Execution

In combination with the <thread> library, we can use durations to cause our application to go to sleep for some time. This is available as the sleep_for function on std::this_thread, which we have access to if we #include <thread>

Below, we use this to insert a 5-second pause between the "Starting" and "Done" logs:

#include <chrono>
#include <iostream>
#include <thread>

int main() {
  using namespace std::chrono;

  std::cout << "Starting\n";
  std::this_thread::sleep_for(5s);
  std::cout << "Done!";
}
Starting
Done!

Chrono Clocks

A clock is a source of time information. By default, chrono offers 3 clocks:

  • The system_clock uses data from the operating system's built-in clock. For most cases, using this is the simplest approach.
  • The steady_clock is designed not to 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.
  • The 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.

Chrono Time Points

Using data from a clock, we can generate a value representing a specific point in time. Chrono represents time points using the std::chrono::time_point type. For example, to get the current time point from the system clock, we can do this:

#include <chrono>

int main() {
  using namespace std::chrono;
  time_point CurrentTime {
    system_clock::now()
  };
}

We can also add or remove durations to time points. Below, we generate a time point 3 weeks in the past:

#include <chrono>

int main() {
  using namespace std::chrono;
  time_point ThreeWeeksAgo {
    system_clock::now() - weeks(3)
  };
}

We can generate a duration by comparing time points using the - operator. Below, we generate two time points, approximately 5 seconds apart. We then store the difference between those two time points as a duration, which we output as milliseconds using duration_cast:

#include <chrono>
#include <iostream>
#include <thread>

int main() {
  using namespace std::chrono;

  time_point StartTime{system_clock::now()};
  std::this_thread::sleep_for(seconds(5));
  time_point EndTime{system_clock::now()};

  // Create a duration from two time points
  duration RunningTime{EndTime - StartTime};

  std::cout << "Time difference in ms: "
    << duration_cast<milliseconds>(
      RunningTime).count();
}
Time difference in ms: 5007

Clock Drift

There is something to bear in mind when using sleep_for, and almost any other way of causing our application to pause.

That is, our applications will always sleep for slightly longer than we requested. This is 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 the previous run, we asked our application to sleep for 5 seconds, but it will typically sleep for around 5.007 seconds.

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 is updated every second, and updates are delayed by 0.007 seconds on average, our clock would lose around 10 minutes of accuracy every 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 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.

Using std::time_t

Chrono is a relatively new addition to the language. C++ has an older way of representing times, which is still commonly used. These times are typically stored using the std::time_t type, and we’ll need to be able to interact with both.

Our Chrono clocks have functions that allow us to generate std::time_t objects from chrono::time_point objects. When we're using the system_clock, we can call the system_clock::to_time_t function.

Below, we’re using this to store the current time in a std::time_t called Time:

#include <chrono>
#include <iostream>

int main() {
  using namespace std::chrono;
  time_point StartTime {
    system_clock::now()
  };

  std::time_t Time {
    system_clock::to_time_t(StartTime)
  };

  std::cout << Time;
}

This will output a number, similar to this:

1651075702

Unix Time

The previous output is an example of a UNIX timestamp. This is a commonly used convention when computer systems communicate dates and times with each other.

A Unix timestamp treats any point in time as a single integer. That integer represents the number of seconds between the chosen date, and 00:00:00 UTC on 1st January 1970, sometimes referred to as the Unix epoch

Websites such as unixtimestamp.com allow us to convert human-readable dates to Unix timestamps, and vice versa. We’ll see how to do this within our program later in this lesson.

Creating a time_point from a time_t

The previous example showed how we can create a time_t from a chrono time_point. We can also go in the opposite direction, creating a time_point from a time_t by using the from_time_t functions:

void HandleTime(std::time_t T) {
  using namespace std::chrono;
  time_point TP{system_clock::from_time_t(T)};
  // ...
}

Date and Time Structs (tm)

A Unix timestamp is enough to represent a time, but it’s not a particularly friendly format to work with. An alternative way time points are stored is as tm structs. These represent dates and times in terms of their components - years, months, hours, etc.

Interpreting our std::time_t in this way involves two parts:

  • The standard library’s tm struct gives us a way to create an object for storing these individual components. Our object will have member variables like tm_year, tm_hour, tm_min, and more. The complete list of its variables, and what they mean, are available on a standard library reference such as cppreference.com.
  • The localtime_r function populates a tm object. This function takes two arguments. The first is a pointer to our time_t. The second argument is a pointer to the tm which it will populate:
localtime_r(&TimePoint, &TimeContainer);

localtime_r on Windows

When compiling on Windows, the localtime_r function may result in compilation errors. This can be addressed by using localtime_s instead, and switching the order of the arguments:

// Before
localtime_r(&TimePoint, &TimeContainer);

// After
localtime_s(&TimeContainer, &TimePoint);

Below, we create our tm called TimeContainer, and use the localtime_r function to populate it using data from our time_t, which we’ve called TimeSinceEpoch:

#include <chrono>
#include <iostream>

int main() {
  using namespace std::chrono;
  time_point StartTime {
    system_clock::now()
  };
  std::time_t TimeSinceEpoch {
    system_clock::to_time_t(StartTime)
  };

  tm TimeContainer;
  localtime_s(&TimeContainer, &TimeSinceEpoch);

  std::cout << "The current time is "
    << TimeContainer.tm_hour << ":"
    << TimeContainer.tm_min;
}
The current time is 23:32

Creating a time_t from a tm

If we want to go in the opposite direction, the std::mktime function can help us. We pass it a pointer to a tm and it returns the equivalent time_t:

void HandleTime(std::tm* T) {
  std::time_t Time{std::mktime(T)};
  // ...
}

Using std::put_time and Format Strings

Formatting a date and time to be displayed in a user-friendly way is a surprisingly complex task. With some effort, we could use the various member variables of the tm struct to accomplish it, but there are easier ways. In the next lesson, we’ll cover std::format, which gives us a way to format chrono time_point objects.

In this section, we’ll cover std::put_time, which lets us format std::time_t objects using a similar API

To access std::put_time, we need to #include <iomanip>. We can then call std::put_time, which accepts two arguments:

  • A pointer to our time container
  • A string that describes how we want our time to be formatted

The following shows this in action:

#include <chrono>
#include <iostream>
#include <iomanip>

int main() {
  using namespace std::chrono;
  time_point StartTime {
    system_clock::now()
  };
  time_t TimeSinceEpoch {
    system_clock::to_time_t(StartTime)
  };

  tm TimeContainer;
  localtime_r(&TimeSinceEpoch, &TimeContainer);

  std::cout << std::put_time(
    &TimeContainer,
    "The current time is %r on %A"
  );
}

Running this code, we'd see a user-friendly time output:

The current time is 17:08:22 on Wednesday

The std::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. We cover these tokens in more detail in the next lesson, covering string interpolation. A full list of options is also available from most standard library references, such as cppreference.com.

Putting Everything Together

Let's 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>

void PrintTime(const time_t& Time) {
  tm TimeContainer;
  localtime_s(&TimeContainer, &Time);

  std::cout << std::put_time(
    &TimeContainer,
    "The current time is %r on %A\n");
}

int main() {
  using namespace std::chrono;
  time_point StartTime{system_clock::now()};
  PrintTime(system_clock::to_time_t(StartTime));

  std::this_thread::sleep_for(seconds(5));

  time_point EndTime{system_clock::now()};
  PrintTime(system_clock::to_time_t(EndTime));

  duration RunningTime{
    EndTime - StartTime};

  std::cout << "The running time was "
    << duration_cast<milliseconds>(RunningTime)
         .count()/1000.0 << " 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.012 seconds

Summary

In this lesson, we've explored the essential concepts of handling dates, times, and durations in C++ using the chrono library. We also introduced other representations of time, including Unix timestamps stored in the std::time_t type, and the tm struct.

Key Learnings:

  • Understanding and using std::chrono::duration for representing time intervals in various units.
  • Implementing and manipulating time using the chrono library, including addition, subtraction, and comparison of durations.
  • Utilizing different types of clocks in C++, namely system_clock, steady_clock, and high_resolution_clock, for various time measurement needs.
  • Creating and manipulating std::chrono::time_point objects to represent specific points in time.
  • Converting time points to and from std::time_t and using tm structures for a more readable time format.

Preview of the Next Lesson

In the upcoming lesson, we'll dive into the world of string interpolation in C++, focusing on the powerful std::format and std::print functions introduced in C++20.

Building on our knowledge from this lesson, we'll see how these functions can be seamlessly integrated with the chrono library to format and display dates and times in a more user-friendly manner.

Key Topics Include:

  • Introduction to std::format and std::print functions in C++20 for advanced string formatting.
  • Understanding format specifiers and how they can be used to customize the output.
  • Practical examples demonstrating the use of std::format and std::print in real-world scenarios.
  • Techniques for incorporating chrono library types with std::format to output time and date information.

Was this lesson useful?

Next Lesson

String Interpolation

A detailed guide to string formatting using C++20's std::format(), and C++23's std::print()
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Updated
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
Odds and Ends
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, unlimited access

This course includes:

  • 60 Lessons
  • Over 200 Quiz Questions
  • 95% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

String Interpolation

A detailed guide to string formatting using C++20's std::format(), and C++23's std::print()
3D Character Concept Art
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved