JavaRush /Курсы /C++ SELF /time_point и

time_point и system_clock — «момент времени»

C++ SELF
66 уровень , 1 лекция
Открыта

1. Почему «момент» — это отдельный тип, а не просто число

Когда вы впервые слышите «момент времени», мозг предлагает ленивое решение: «ну это же просто количество миллисекунд, давайте long long». И оно даже иногда работает. Ровно до тех пор, пока в проект не приходит второй разработчик, третий формат хранения, и кто-то случайно не начинает складывать «момент времени» с «моментом времени», получая математически бессмысленную штуку — как если бы вы сложили два адреса домов и ожидали получить адрес магазина.

В std::chrono момент времени моделируется типом time_point. Смысл простой: time_point — это точка на шкале времени, привязанная к конкретным «часам» (Clock). Без часов момент неполный, потому что разные часы могут иметь разную точку отсчёта и даже разное поведение.

Можно запомнить короткое правило, которое спасает от половины ошибок:

Сущность Вопрос Тип
Интервал «Сколько прошло / сколько ждать?»
std::chrono::duration
Момент «Когда произошло / к какому времени привязано?»
std::chrono::time_point

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. Это именно момент. Он полезен не сам по себе (распечатать его «как есть» неудобно), а как строительный блок для:

  1. хранения момента (например, «когда создана задача»),
  2. сравнения моментов (например, «что было раньше»),
  3. вычисления интервала между моментами (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()).

Схема такая:

  1. число миллисекунд → std::chrono::milliseconds{...}
  2. 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, и он будет в следующей лекции.

1
Задача
C++ SELF, 66 уровень, 1 лекция
Недоступна
Дробление эпохи
Дробление эпохи
1
Задача
C++ SELF, 66 уровень, 1 лекция
Недоступна
Дуэль меток
Дуэль меток
1
Задача
C++ SELF, 66 уровень, 1 лекция
Недоступна
Сдвиг времени
Сдвиг времени
1
Задача
C++ SELF, 66 уровень, 1 лекция
Недоступна
План запусков
План запусков
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ