1. Введение
Если вы уже ловили себя на мысли «ну время же одно, зачем столько сущностей», то поздравляю: вы мыслите как нормальный человек, а не как разработчик стандартной библиотеки. Но у компьютера «время» бывает двух разных смыслов, и эти смыслы в коде очень легко перепутать. Именно поэтому в std::chrono есть разные часы (clock): одни нужны, чтобы понимать дату/время в жизни человека, другие — чтобы честно измерять сколько длилась операция.
Представьте две ситуации.
В первой вы добавляете задачу в ваш консольный менеджер задач и хотите записать: «создано 16 января 2026, 10:15:03». Это календарное время. Ему важно совпадать с часами на компьютере, с часовым поясом пользователя, с тем, что он видит в интерфейсе.
Во второй вы хотите измерить: «сколько миллисекунд заняла сортировка списка задач» или «сколько времени ушло на чтение файла». Это измерение длительности. И тут календарность обычно вообще не нужна. Более того, она может мешать, потому что календарное время иногда скачет.
2. system_clock: календарное время, которое может прыгать
Когда программист говорит «текущее время», чаще всего он имеет в виду именно std::chrono::system_clock. Это часы, которые предназначены для календарного времени: они соответствуют тому, что показывает система (и в целом тому, что человек ожидает увидеть). У этих часов есть понятие эпохи, и их можно преобразовать в std::time_t, а значит — более-менее легко форматировать в «2026-01-16 10:15:03» (подробный вывод — это тема соседней лекции дня).
При этом у system_clock есть важная особенность: время может быть изменено извне. Например, пользователь вручную поправил часы, система синхронизировалась по NTP, произошёл переход на/с летнего времени (в некоторых конфигурациях), администратор на сервере решил «подкрутить» время. Для календаря это нормально: календарь должен отражать реальность (пусть и странную). Для измерений длительности это плохо: вы можете получить отрицательную длительность или «внезапно огромную».
Мини-идея в коде такая: system_clock хорош для «когда», но подозрителен для «сколько длилось».
Небольшой фрагмент для нашего учебного приложения TaskBook (условный менеджер задач), где мы сохраняем момент создания задачи как «миллисекунды с эпохи» — это как раз корректная роль system_clock:
#include <chrono>
long long now_ms_since_epoch() {
const auto now = std::chrono::system_clock::now();
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()
);
return ms.count();
}
Здесь важно, что мы явно приводим time_since_epoch() к миллисекундам, потому что count() сам по себе не обязан быть «в миллисекундах». (Эта осторожность — прямое продолжение темы duration_cast из прошлой лекции дня.)
3. steady_clock: часы для измерений, которые не «откатываются назад»
Теперь переходим к герою лекции — std::chrono::steady_clock. Это часы для измерения длительности. Ключевая идея steady_clock в том, что он монотонный: «сейчас» у этих часов не должно становиться меньше, чем было раньше. Проще говоря, время у steady_clock не обязано совпадать с календарём, но оно должно быть хорошей линейкой.
Когда вы измеряете скорость выполнения кода, вам важно, чтобы «старт» был раньше «финиша» (логично, да). Календарные часы могут неожиданно поехать назад, и тогда finish - start получится отрицательным. steady_clock сделан именно для того, чтобы так не происходило.
Типовой паттерн измерения выглядит скучно, но он работает годами и обычно переживает даже смену команды:
#include <chrono>
std::chrono::milliseconds measure_demo() {
const auto start = std::chrono::steady_clock::now();
// ... делаем работу ...
const auto finish = std::chrono::steady_clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(finish - start);
}
Обратите внимание на смысловую красоту: мы вычитаем два момента (time_point), и получаем длительность (duration). Это «правильная арифметика времени», и она читается почти как формула.
4. Самое важное сравнение: «когда произошло» vs «сколько заняло»
В этом месте полезно на секунду притормозить, потому что в голове новичка эти два мира очень легко смешиваются. Если смешать — компилятор иногда вас спасёт (не всегда), а иногда вы получите баг, который проявится раз в год и строго по вторникам.
Ниже — таблица, которая фиксирует роль часов. Её стоит воспринимать как «шпаргалку на монитор», а не как теорию ради теории.
| Вопрос, который вы задаёте | Правильный тип часов | Почему |
|---|---|---|
| «Который сейчас час (для человека)?» | |
Это календарное время, привязанное к настройкам системы |
| «Когда задача была создана?» | |
У события должна быть дата/время, совпадающие с реальностью |
| «Сколько времени заняла операция?» | |
Нужна монотонная линейка без скачков |
| «Можно ли это хранить в файле и читать завтра?» | system_clock (в виде time_t или ms since epoch) | У steady_clock нет смысла «в абсолютных датах» |
И вот главный ориентир (без фанатизма, но как правило): таймстемпы — через system_clock, измерения — через steady_clock.
Почему нельзя сравнивать system_clock и steady_clock напрямую
Очень частая ошибка: «а давайте я возьму start от system_clock, finish от steady_clock, ну а что такого, оба же time_point». И вот тут std::chrono показывает свою суперсилу: разные Clock — разные системы отсчёта, и типы у них разные.
Даже если два значения выглядят как «точки времени», они не взаимозаменяемы. У них потенциально разная эпоха, разная природа измерения, разные гарантии. Смешивать их в выражениях — почти всегда логическая ошибка, поэтому компилятор обычно не даст вам этого сделать.
Пример того, что не нужно делать (и это хорошо, что оно не компилируется):
#include <chrono>
int main() {
const auto a = std::chrono::system_clock::now();
const auto b = std::chrono::steady_clock::now();
// auto diff = b - a; // так нельзя: разные часы
}
Эта «несовместимость» — не ограничение ради издевательства. Это защита от бага, который потом очень трудно объяснить человеку, далёкому от C++: «почему время стало отрицательным?».
Почему system_clock плох для измерений
Очень хочется сказать: «ну я же могу измерить так: auto start = system_clock::now(); ... auto elapsed = now - start; — и оно покажет число». И действительно, на большинстве компьютеров в большинстве ситуаций оно покажет «что-то похожее на правду». Проблема в том, что это будет правдой ровно до того момента, пока система не решит, что время надо подправить.
Представьте сервер, который синхронизирует время по сети. Он может сделать коррекцию назад, и тогда ваша измеренная длительность внезапно станет отрицательной. Или наоборот — подправит вперёд, и вы получите «операция заняла 5 минут», хотя она заняла 20 миллисекунд. И самое неприятное: это может случиться не у вас, а у пользователя, и воспроизвести будет сложно.
Небольшой пример, который выглядит невинно, но методически неверный:
#include <chrono>
std::chrono::milliseconds bad_measurement() {
const auto start = std::chrono::system_clock::now();
// ... работа ...
const auto finish = std::chrono::system_clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(finish - start);
}
Он может «работать», но контракт у него слабый: вы измеряете длительность часами, которые предназначены для календаря и могут корректироваться. Поэтому в коде, который вы хотите считать надёжным, лучше сразу привыкать: измерение = steady_clock.
5. Встраиваем время в приложение TaskBook
Сейчас мы добавим две маленькие, но очень практичные детали в наш учебный консольный проект. Пусть это будет TaskBook: список задач, который мы уже умеем хранить (например, в JSON из прошлого дня) и выводить в консоль. Сегодня нас интересует не формат хранения как таковой, а что именно мы храним про время и как измеряем скорость.
Начнём с модели задачи: добавим поле created_at_ms. По смыслу это абсолютное время создания, то есть берём system_clock и сохраняем «миллисекунды с эпохи».
#include <string>
struct Task {
int id = 0;
std::string title;
long long created_at_ms = 0; // ms since epoch (system_clock)
};
Теперь напишем функцию создания задачи, которая проставляет это поле. Обратите внимание: мы не храним time_point прямо в структуре, а кладём обычное число. Это проще для сериализации и для печати, и соответствует идее «на границе программы — число, внутри логики — chrono-типы».
#include <chrono>
#include <string>
Task make_task(int id, const std::string& title) {
Task t;
t.id = id;
t.title = title;
t.created_at_ms = now_ms_since_epoch();
return t;
}
Теперь добавим «измеритель» для одной операции. Например, мы хотим понять, сколько времени занимает добавление задачи (да, операция дешёвая, но это учебный пример; настоящие причины будут позже — например чтение/запись, сортировки, фильтрации).
#include <chrono>
#include <vector>
std::chrono::milliseconds add_task_timed(std::vector<Task>& tasks, Task t) {
const auto start = std::chrono::steady_clock::now();
tasks.push_back(t);
const auto finish = std::chrono::steady_clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(finish - start);
}
А теперь — аккуратный вывод, чтобы увидеть, что происходит:
#include <iostream>
int main() {
std::vector<Task> tasks;
auto t = make_task(1, "Прочитать про chrono");
auto elapsed = add_task_timed(tasks, t);
std::cout << "Added task in " << elapsed.count() << " ms\n"; // Added task in 0 ms
}
Здесь нормально, что вы увидите 0 ms: операция слишком быстрая, и миллисекунды грубоваты. Важнее другое: вы использовали правильные часы для правильного вопроса. Если захотите точнее — можно печатать микросекунды, но это уже вопрос выбранной единицы, а не выбора часов.
6. Ментальная модель: две шкалы времени
Иногда легче всего понять разницу между steady_clock и system_clock, если представить две линейки.
Первая линейка — это календарь на стене. Сегодня 16 января 2026 года. Завтра будет 17 января. Но если кто-то возьмёт и перевесит календарь (или на телефоне поменяет дату), то «сегодня» для вас в этот момент изменится. Это неприятно, но в календарном смысле допустимо: календарь отражает соглашение.
Вторая линейка — это секундомер. Он не обязан говорить, какое сегодня число. Он обязан честно тикать вперёд, чтобы вы могли сказать: «пробежал 100 метров за 14 секунд». Если секундомер начнёт вдруг тикать назад, это перестанет быть секундомер и превратится в философский прибор, который измеряет судьбу.
Эту же мысль можно закрепить схемой:
flowchart TD
A[Событие в мире: 'задача создана'] --> B["system_clock::now()"]
B --> C[Храним как ms since epoch]
D[Операция в коде: 'сортировка задач'] --> E["steady_clock::now() start"]
E --> F[...работа...]
F --> G["steady_clock::now() finish"]
G --> H[elapsed = finish - start]
Если вы держите в голове эту развилку, вы почти автоматически выбираете правильные часы под задачу.
7. Типичные ошибки при сравнении steady_clock и system_clock
Ошибка №1: измерять длительность через system_clock, потому что «так проще».
Обычно это заканчивается тем, что код действительно выглядит проще… до первого странного случая на реальной машине. system_clock может корректироваться системой, поэтому измерения через него потенциально нестабильны. Для длительности используйте steady_clock, а system_clock оставляйте для таймстемпов и календарных отметок.
Ошибка №2: хранить steady_clock::time_point в файле или в модели данных как «момент времени».
steady_clock хорош как внутренняя линейка, но его «абсолютное значение» не обязано соответствовать календарю и даже не обязано быть сопоставимым между запусками программы. Если вам нужно хранить момент времени в данных (а тем более сериализовать), используйте system_clock и сохраняйте «секунды/миллисекунды с эпохи».
Ошибка №3: пытаться смешивать time_point разных часов в арифметике.
Новички иногда ожидают, что раз это «точка времени», то её можно вычитать из любой другой «точки времени». Но в std::chrono тип time_point включает в себя тип часов, и это намеренная защита. Если у вас два time_point от разных Clock, это две разные системы отсчёта, и их нельзя корректно вычитать напрямую.
Ошибка №4: выводить count() без явной единицы и потом неправильно трактовать число.
Даже если вы правильно выбрали часы, можно легко ошибиться на «последнем метре»: вывести elapsed.count() и забыть, миллисекунды это или микросекунды. Хорошая привычка — перед выводом всегда приводить к нужной единице через duration_cast и печатать суффикс (" ms", " us").
Ошибка №5: «оптимизировать» измерение слишком рано, выбирая наносекунды везде.
Иногда хочется печатать всё в nanoseconds, потому что «так точнее». На практике вы получите большие числа, шум измерений и ложную уверенность. Для UI и логов чаще подходят миллисекунды, для очень быстрых операций — микросекунды. Главное — чтобы единица соответствовала смыслу, а не желанию выглядеть круто.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ