1. Почему «момент» — это отдельный тип, а не просто число
Когда вы впервые слышите «момент времени», мозг предлагает ленивое решение: «ну это же просто количество миллисекунд, давайте long long». И оно даже иногда работает. Ровно до тех пор, пока в проект не приходит второй разработчик, третий формат хранения, и кто-то случайно не начинает складывать «момент времени» с «моментом времени», получая математически бессмысленную штуку — как если бы вы сложили два адреса домов и ожидали получить адрес магазина.
В std::chrono момент времени моделируется типом time_point. Смысл простой: time_point — это точка на шкале времени, привязанная к конкретным «часам» (Clock). Без часов момент неполный, потому что разные часы могут иметь разную точку отсчёта и даже разное поведение.
Можно запомнить короткое правило, которое спасает от половины ошибок:
| Сущность | Вопрос | Тип |
|---|---|---|
| Интервал | «Сколько прошло / сколько ждать?» | |
| Момент | «Когда произошло / к какому времени привязано?» | |
system_clock: «настоящее время», но с характером
std::chrono::system_clock — это часы, которые представляют «текущее календарное время» системы. То есть то самое «сейчас», которое видит человек на экране, в логах, в файловых датах (в общем смысле), в сообщениях «создано 2 минуты назад».
При этом у system_clock есть важная человеческая особенность: система может корректировать время. Например, синхронизация по сети, ручная смена времени пользователем, переходы и настройки — всё это может влиять на «календарные часы». Поэтому system_clock идеально подходит для «что показать пользователю и что записать в лог как дату», но плохо подходит для точных измерений длительности операций. Точные измерения мы будем делать позже на steady_clock, а сегодня честно держим фокус именно на «моменте».
С технической стороны system_clock — это тип, у которого есть вложенный тип time_point, и статический метод now(), который возвращает «момент сейчас».
2. time_point: получение и привязка к эпохе
Как получить time_point
Первое знакомство с time_point обычно выглядит как встреча с чем-то «шаблонным и страшным». На самом деле пользоваться им можно очень просто, особенно если применять auto.
Вот базовый минимальный пример:
#include <chrono>
#include <iostream>
int main() {
auto now = std::chrono::system_clock::now();
(void)now; // пока просто зафиксировали: now — это time_point
std::cout << "Got current time_point\n"; // Got current time_point
}
Ключевой момент здесь в том, что now — не число и даже не duration. Это именно момент. Он полезен не сам по себе (распечатать его «как есть» неудобно), а как строительный блок для:
- хранения момента (например, «когда создана задача»),
- сравнения моментов (например, «что было раньше»),
- вычисления интервала между моментами (time_point - time_point).
И да: у chrono это всё сделано так, чтобы арифметика и преобразования были максимально безопасными на уровне типов; в стандарте отдельно двигались в сторону того, чтобы операции duration/time_point могли быть constexpr там, где это разумно.
Epoch и time_since_epoch()
Очень часто момент времени нужно сохранить «в виде числа»: например, в файле, в базе данных, в JSON или просто в памяти как простое поле, которое легко сортировать и сравнивать.
Для этого time_point можно превратить в duration относительно некоторой эпохи (epoch) — точки отсчёта, которую выбирают «часы».
Делается это так:
#include <chrono>
#include <iostream>
int main() {
auto now = std::chrono::system_clock::now();
auto since_epoch = now.time_since_epoch(); // duration
std::cout << "duration count (raw) = " << since_epoch.count() << '\n';
}
Подвох тут один, но очень популярный: since_epoch.count() — это «сырое число» в единицах duration-типа, который использует system_clock. А этот тип и его «тиковая частота» зависят от реализации (платформы/компилятора). Поэтому печатать или сохранять count() без явного приведения — почти всегда плохая идея.
Правильный подход: привести длительность от эпохи к нужной единице. Обычно выбирают миллисекунды, иногда — секунды.
Пример с миллисекундами:
#include <chrono>
#include <iostream>
int main() {
auto now = std::chrono::system_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()
);
long long ms_since_epoch = ms.count();
std::cout << ms_since_epoch << " ms since epoch\n";
}
Здесь мы делаем две важные вещи. Во‑первых, явно задаём единицы (milliseconds). Во‑вторых, называем переменную так, чтобы единицы были в названии (ms_since_epoch). Это не занудство — это профилактика багов класса «почему всё в 1000 раз больше».
4. Арифметика с time_point: что можно, а что бессмысленно
Когда в коде появляется «момент», руки тянутся к привычной арифметике. И тут важно заранее договориться с собой (и с компилятором).
С time_point логика такая:
- time_point + duration даёт новый time_point (момент сдвинулся вперёд/назад).
- time_point - time_point даёт duration (сколько времени между моментами).
- time_point + time_point — бессмыслица, и стандартная библиотека вам это не даст сделать.
Посмотрим на нормальный пример: «через 5 секунд после текущего момента».
#include <chrono>
#include <iostream>
int main() {
auto now = std::chrono::system_clock::now();
auto later = now + std::chrono::seconds{5};
auto diff = later - now; // duration
auto s = std::chrono::duration_cast<std::chrono::seconds>(diff);
std::cout << s.count() << " s\n"; // 5 s
}
Здесь очень красивый смысл: разность двух моментов — это интервал. То есть мы наконец-то получаем «сколько прошло» из «когда было».
Отдельно отмечу момент для любителей «оптимизировать типы»: иногда хочется хранить время как unsigned-число («оно же не отрицательное!»). На практике это часто рождает неприятные сюрпризы при вычитании и сравнениях; в стандартных обсуждениях даже есть отдельные баги/дефекты, связанные с вычитанием time_point с unsigned duration.
В студенческом коде правило простое: если вы не уверены на 200%, не превращайте время в unsigned. Храните «миллисекунды с эпохи» в обычном знаковом целочисленном типе (long long или std::int64_t).
5. std::time_t: мост в «старый мир»
Хотя std::chrono — современный подход, «старый мир» C API времени никуда не делся. Встречаются функции, которые принимают/возвращают std::time_t. Это обычно «секунды с эпохи» (с точностью до секунд).
У system_clock есть готовые функции для мостика:
- std::chrono::system_clock::to_time_t(time_point)
- std::chrono::system_clock::from_time_t(time_t)
Минимальный пример:
#include <chrono>
#include <ctime>
#include <iostream>
int main() {
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::cout << t << " seconds since epoch\n";
}
Важно понимать ограничение: time_t обычно хранит секунды, то есть миллисекунды при таком преобразовании «схлопнутся». Для логов «в какую секунду произошло» этого часто достаточно, а для точных меток (например, «нажали кнопку в 12:34:56.789») — уже нет.
6. Практический пример: created_at и «возраст» задачи
Чтобы тема не оставалась абстрактной, давайте развивать маленькое консольное приложение, которое мы в курсе постоянно улучшаем. Пусть это будет простой «список дел»: задачи хранят текст и момент создания. Мы пока не сохраняем ничего в файлы и не работаем с форматами — сегодня нам достаточно научиться ставить метку времени и сравнивать моменты.
Сделаем модель задачи, которая хранит момент создания как «миллисекунды с эпохи». Почему не time_point прямо в структуре? Можно и так, но число проще сериализовать и печатать, а также оно не привязано к конкретному внутреннему representation. Мы осознанно фиксируем единицы: миллисекунды.
#include <string>
struct Task {
int id = 0;
std::string text;
long long created_ms = 0; // ms since epoch
};
Теперь напишем функцию, которая получает «сейчас» в миллисекундах:
#include <chrono>
long long now_ms_since_epoch() {
auto now = std::chrono::system_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()
);
return ms.count();
}
Обратите внимание: функция возвращает число, но внутри делает всё «по-взрослому»: берёт time_point, превращает его в duration от эпохи, приводит к миллисекундам и только потом достаёт count().
Добавим создание задачи:
#include <iostream>
#include <vector>
int main() {
std::vector<Task> tasks;
Task t;
t.id = 1;
t.text = "Buy milk (or compile errors)";
t.created_ms = now_ms_since_epoch();
tasks.push_back(t);
std::cout << "Created task at " << t.created_ms << " ms\n";
}
Смысл этого шага в том, что теперь у каждой задачи есть «паспорт»: момент создания. И мы можем, например, сортировать задачи по created_ms, сравнивать, фильтровать, находить «самые новые» — всё это без ручных пересчётов.
Из числа обратно в time_point
Иногда вы храните момент как число (например, created_ms), но вам нужно снова получить time_point, чтобы сделать «честную chrono-арифметику» (например, сравнивать с system_clock::now()).
Схема такая:
- число миллисекунд → std::chrono::milliseconds{...}
- system_clock::time_point{milliseconds{...}}
Вот маленькая утилита:
#include <chrono>
std::chrono::system_clock::time_point tp_from_ms(long long ms_since_epoch) {
return std::chrono::system_clock::time_point{
std::chrono::milliseconds{ms_since_epoch}
};
}
А теперь можем вычислить «возраст задачи» в секундах, не ломая типы:
#include <chrono>
std::chrono::seconds task_age_seconds(const Task& t) {
auto created_tp = tp_from_ms(t.created_ms);
auto now = std::chrono::system_clock::now();
auto age = now - created_tp; // duration
return std::chrono::duration_cast<std::chrono::seconds>(age);
}
Тут есть тонкая, но важная мысль: мы не «вычитаем миллисекунды из миллисекунды» как голые числа, а работаем в chrono-типах. Это особенно приятно, когда единицы могут меняться: сегодня печатаете секунды, завтра — минуты, а логика остаётся корректной.
7. Схема: time_point, epoch и duration
Иногда полезно один раз увидеть это как схему и перестать бояться:
flowchart LR
TP["time_point (момент)"] -->|"time_since_epoch()"| D["duration (интервал от epoch)"]
D -->|duration_cast<milliseconds>| MS["milliseconds"]
MS -->|"count()"| N["целое число (ms since epoch)"]
Смысл: time_point удобно для арифметики моментов, duration удобно для интервалов, «число» удобно для хранения/передачи. И вы можете ходить туда-сюда, но лучше делать это осознанно и с фиксированными единицами.
8. Типичные ошибки при работе с time_point и system_clock
Ошибка №1: путать time_point и duration (и пытаться делать «интервалы» как моменты).
Эта ошибка обычно выглядит так: вы храните «таймаут 500мс» как system_clock::time_point, а потом не понимаете, что с ним делать. Таймаут — это интервал, ему место в duration. Момент (time_point) нужен, когда вы знаете «когда именно»: «время создания задачи», «время последнего сохранения», «дедлайн».
Ошибка №2: использовать time_since_epoch().count() без duration_cast, а потом считать, что это миллисекунды.
Классическая ловушка: код компилируется, число печатается, а через месяц выясняется, что на одной машине это «наносекунды», на другой — «что-то ещё». Правильная привычка: всегда приводить к явной единице (milliseconds, seconds) и только потом брать count().
Ошибка №3: складывать два time_point или «прибавлять число» без указания единиц.
Сложить два момента времени — это как сложить «вторник» и «четверг»: словесно можно, смыслово — нет. В chrono нормальная операция — «момент + интервал». Поэтому если вы хотите «через 5 секунд», прибавляйте std::chrono::seconds{5}, а не + 5 (время не любит, когда к нему относятся как к int).
Ошибка №4: хранить timestamp в unsigned и ловить странности при вычитании.
Кажется логичным: «время же не отрицательное». На практике вычитание и сравнения могут начать вести себя неожиданно, особенно если где-то появляется отрицательный интервал или разные типы длительностей. В стандартной библиотеке и обсуждениях встречались отдельные тонкости вокруг вычитания time_point с unsigned duration.
Без острой необходимости используйте знаковый тип для «миллисекунд с эпохи».
Ошибка №5: использовать system_clock для измерения длительности операций и удивляться отрицательным интервалам.
Это почти неизбежно случается, когда система подправила время назад, а вы измеряете «сколько заняло» через календарные часы. Сегодня мы это только фиксируем как риск. Правильный инструмент для измерений — steady_clock, и он будет в следующей лекции.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ