skip to content
Mehdi Mehdikhani
Table of Contents

std::chrono in C++11 finally brought type-safe time handling to C++. No more confusing seconds vs milliseconds bugs or platform-specific time APIs.

What std::chrono Actually Does

It provides type-safe time points, durations, and clocks:

#include <chrono>
using namespace std::chrono;
// Durations with different units
auto ms = 500ms; // 500 milliseconds
auto sec = 5s; // 5 seconds
auto min = 2min; // 2 minutes
// Time points
auto now = steady_clock::now();
auto later = now + 10s;
// Duration between time points
auto elapsed = later - now; // 10 seconds

The type system prevents unit confusion at compile time.

The Three Main Components

std::chrono has three main parts:

  1. Durations: Represent time intervals
  2. Time points: Represent specific moments in time
  3. Clocks: Sources of time points
// Duration - a span of time
duration<int, std::milli> ms_duration(500); // 500 milliseconds
// Time point - a specific moment
time_point<steady_clock> start = steady_clock::now();
// Clock - provides current time
auto system_time = system_clock::now();
auto steady_time = steady_clock::now();
auto high_res_time = high_resolution_clock::now();

When I Actually Use std::chrono

Most of the time, I use std::chrono for:

  1. Timing operations: Measuring how long things take:
class PerformanceTimer {
std::chrono::steady_clock::time_point start_;
public:
PerformanceTimer() : start_(std::chrono::steady_clock::now()) {}
void reset() {
start_ = std::chrono::steady_clock::now();
}
auto elapsed() const {
return std::chrono::steady_clock::now() - start_;
}
double elapsed_seconds() const {
auto duration = elapsed();
return std::chrono::duration<double>(duration).count();
}
};
// Usage
PerformanceTimer timer;
expensive_operation();
std::cout << "Operation took: " << timer.elapsed_seconds() << " seconds\n";
  1. Timeouts and delays: Sleep and timeout handling:
void wait_for_connection(int timeout_seconds) {
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_seconds);
while (std::chrono::steady_clock::now() < deadline) {
if (is_connected()) {
return; // Connected successfully
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
throw std::runtime_error("Connection timeout");
}
void rate_limited_operation() {
static auto last_call = std::chrono::steady_clock::now();
const auto min_interval = std::chrono::milliseconds(100);
auto now = std::chrono::steady_clock::now();
auto elapsed = now - last_call;
if (elapsed < min_interval) {
std::this_thread::sleep_for(min_interval - elapsed);
}
// Do the operation
last_call = std::chrono::steady_clock::now();
}
  1. Logging with timestamps: Adding time information to logs:
class Logger {
public:
void log(const std::string& level, const std::string& message) {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()) % 1000;
std::cout << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
std::cout << '.' << std::setfill('0') << std::setw(3) << ms.count();
std::cout << " [" << level << "] " << message << std::endl;
}
};
// Output: 2022-09-15 14:30:25.123 [INFO] Application started
  1. Animation and game loops: Frame timing:
class GameLoop {
std::chrono::steady_clock::time_point last_frame_;
std::chrono::duration<double> target_frame_time_{1.0 / 60.0}; // 60 FPS
public:
void run() {
last_frame_ = std::chrono::steady_clock::now();
while (running_) {
auto frame_start = std::chrono::steady_clock::now();
auto delta_time = frame_start - last_frame_;
update(std::chrono::duration<double>(delta_time).count());
render();
auto frame_end = std::chrono::steady_clock::now();
auto frame_duration = frame_end - frame_start;
if (frame_duration < target_frame_time_) {
std::this_thread::sleep_for(target_frame_time_ - frame_duration);
}
last_frame_ = frame_start;
}
}
};

Duration Conversions

Conversions between units are explicit and safe:

auto seconds = 5s;
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(seconds);
// milliseconds.count() == 5000
auto ms = 1500ms;
auto sec = std::chrono::duration_cast<std::chrono::seconds>(ms);
// sec.count() == 1 (truncated, not rounded)
// For exact conversions without truncation:
auto precise_seconds = std::chrono::duration<double>(ms);
// precise_seconds.count() == 1.5

Different Clocks for Different Purposes

Choose the right clock for your use case:

// system_clock - wall clock time, can jump backwards/forwards
auto wall_time = std::chrono::system_clock::now();
// steady_clock - monotonic clock, never goes backwards (best for timing)
auto monotonic_time = std::chrono::steady_clock::now();
// high_resolution_clock - highest precision available (often alias to steady_clock)
auto precise_time = std::chrono::high_resolution_clock::now();

Real World Example

Here’s a connection pool with timeout handling:

template<typename Connection>
class ConnectionPool {
std::queue<std::unique_ptr<Connection>> available_;
std::mutex mutex_;
std::condition_variable cv_;
std::chrono::milliseconds default_timeout_{5000}; // 5 second timeout
public:
std::unique_ptr<Connection> acquire(std::chrono::milliseconds timeout = {}) {
if (timeout == std::chrono::milliseconds::zero()) {
timeout = default_timeout_;
}
std::unique_lock<std::mutex> lock(mutex_);
auto deadline = std::chrono::steady_clock::now() + timeout;
while (available_.empty()) {
if (cv_.wait_until(lock, deadline) == std::cv_status::timeout) {
throw std::runtime_error("Connection pool timeout");
}
}
auto conn = std::move(available_.front());
available_.pop();
return conn;
}
void release(std::unique_ptr<Connection> conn) {
{
std::lock_guard<std::mutex> lock(mutex_);
available_.push(std::move(conn));
}
cv_.notify_one();
}
};
// Usage
ConnectionPool<DatabaseConnection> pool;
auto conn = pool.acquire(2s); // 2 second timeout

Literals for Convenience

C++14 added time literals that make code much cleaner:

using namespace std::chrono_literals;
// Before literals
std::chrono::milliseconds ms(500);
std::chrono::seconds sec(30);
// With literals
auto ms = 500ms;
auto sec = 30s;
auto min = 5min;
auto hour = 2h;

The Pattern I Follow

I use steady_clock for timing measurements and system_clock when I need actual calendar time:

// For measuring elapsed time
auto start = std::chrono::steady_clock::now();
do_work();
auto elapsed = std::chrono::steady_clock::now() - start;
// For timestamps and logging
auto timestamp = std::chrono::system_clock::now();
log_event(timestamp, "Something happened");
// Always use the literal suffixes when available
std::this_thread::sleep_for(100ms);
auto timeout = 5s;

Common Gotchas

  1. Integer truncation: duration_cast truncates, doesn’t round
  2. Clock choice: Use steady_clock for intervals, system_clock for timestamps
  3. Precision loss: Be careful converting from high to low precision

std::chrono eliminates timing bugs and makes time handling much more robust. It’s verbose at first, but the type safety is worth it.