1. Почему «хранить время» — это маленький контракт
Когда вы впервые добавляете время в программу, есть сильный соблазн сделать что-то вроде long long t = ...; и дальше жить спокойно. Ровно до момента, когда вы открываете сохранённые данные и внезапно понимаете: это секунды? миллисекунды? время по UTC? локальное? относительно старта программы? относительно «эпохи»?
В программировании время — это не только «текущее число», это ещё и договорённость о смысле этого числа. Хорошая новость: чтобы начать нормально, вам не нужно погружаться в часовые пояса и календарную арифметику. Достаточно выбрать одну-две простые стратегии и придерживаться их, как будто это ваш мини-стандарт проекта.
В этой лекции мы сделаем две практические вещи: выберем простые форматы хранения (число или строка) и настроим одинаковую конвертацию time_point ↔ число ↔ строка.
Два слоя: «как храню» и «как показываю»
Важно не смешивать эти слои. Когда программа хранит время, ей чаще всего нужно, чтобы было удобно сравнивать, сортировать, сериализовать и не терять точность. Когда программа показывает время человеку, ей нужно, чтобы было читабельно и привычно.
Поэтому здоровая привычка звучит так: внутри логики живём в std::chrono (или в чётко определённом числовом формате), а на границе (вывод на экран, запись в файл, JSON) конвертируем в «человеческий» вид.
Если попытаться хранить сразу «красивую строку», вы быстро поймаете себя на том, что сортировка становится странной, сравнения — медленными, а парсинг — болезненным. Если попытаться показывать пользователю «миллисекунды с эпохи», пользователь посмотрит на вас как на калькулятор, который решил стать философом.
2. Хранение момента как число от эпохи
Самый практичный и популярный путь для учебных и многих прикладных проектов: хранить момент времени как целое число, означающее «сколько единиц прошло с эпохи». Эпоха для system_clock обычно соответствует Unix epoch (1970-01-01 00:00:00 UTC), но нам сейчас важнее другое: выбираем единицу и больше её не путаем.
Чаще всего выбирают одно из двух:
| Единица хранения | Типичный тип | Плюсы | Минусы |
|---|---|---|---|
| секунды | std::time_t или long long | просто, совместимо со старым C API | точность только до секунд |
| миллисекунды | std::int64_t | точнее, удобно для логов/событий | нужно самим конвертировать в time_point |
В этой лекции я буду рекомендовать миллисекунды как «универсальный старт»: это уже достаточно точно для большинства приложений, и при этом всё ещё целое число.
Конвертация time_point ↔ миллисекунды с эпохи
Дальше — две маленькие функции-переходники: внутри программы мы работаем с std::chrono::system_clock::time_point, а для хранения в модели/файле используем std::int64_t ms_since_epoch.
time_point → ms_since_epoch
Идея простая: time_point можно превратить в duration через time_since_epoch(), а duration привести к миллисекундам через duration_cast. Дальше остаётся только взять count().
#include <chrono>
#include <cstdint>
std::int64_t to_unix_ms(std::chrono::system_clock::time_point tp) {
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
tp.time_since_epoch()
);
return ms.count();
}
Здесь count() возвращает «сырой» числовой тип, но мы его берём только на границе — потому что теперь это уже формат хранения.
ms_since_epoch → time_point
Обратная операция нужна, когда вы загрузили число из файла/JSON и хотите снова работать как нормальный человек, а не как калькулятор.
#include <chrono>
#include <cstdint>
std::chrono::system_clock::time_point from_unix_ms(std::int64_t ms_since_epoch) {
return std::chrono::system_clock::time_point{ std::chrono::milliseconds{ms_since_epoch} };
}
По сути это ровно «момент = эпоха + длительность».
Мини‑правило именования для числового времени
Когда вы храните «голое число», у вас нет типовой защиты, как у std::chrono::milliseconds. Поэтому вы компенсируете это именем.
Имена переменных — это не украшение. Для времени это буквально «страховка от багов». Если вы назвали переменную createdAt, то через месяц вы забудете, что это (секунды? миллисекунды?).
Сравните два подхода:
std::int64_t createdAt = 1737040000000; // непонятно что
std::int64_t created_ms_since_epoch = 1737040000000; // уже лучше
Второй вариант длиннее, зато меньше шансов, что вы сами себя обманете. А программист, который сам себя обманул — самый опасный вид программиста (особенно если у него ещё и доступ к продакшену).
3. Альтернативы: std::time_t и хранение строкой
Иногда миллисекунды вам не нужны. Например, вы сохраняете дату создания заметки, и вам достаточно точности «до секунды». Тогда вполне нормальный вариант — std::time_t. Он удобен тем, что многие функции форматирования ожидают именно его.
Это не религия и не повод спорить «что правильнее». Это выбор под задачу: если точность до секунды вас устраивает, time_t — простой и понятный формат.
Вот как получить time_t из system_clock::now():
#include <chrono>
#include <ctime>
std::time_t now_as_time_t() {
auto now = std::chrono::system_clock::now();
return std::chrono::system_clock::to_time_t(now);
}
А вот как обратно превратить time_t в time_point:
#include <chrono>
#include <ctime>
std::chrono::system_clock::time_point from_time_t(std::time_t t) {
return std::chrono::system_clock::from_time_t(t);
}
Если вы храните time_t, обязательно фиксируйте в голове (и в документации проекта): это секунды.
Когда имеет смысл хранить время строкой
Строка кажется удобной: открыл файл — сразу видишь дату. Но у строки есть цена: её надо генерировать, парсить, валидировать, и она легко превращается в форматный зоопарк («16/01/26», «01-16-2026», «2026.01.16», «пятница, но это неточно»).
Строка хороша, когда вы делаете конфиги, логи для человека, или формат, который человек будет редактировать руками. Для машинного хранения (особенно внутри структур данных) обычно удобнее число.
Тем не менее, минимум, который нам нужен: уметь печатать time_point красиво.
4. Печать календарного времени через put_time
Сейчас мы соберём «классический конвейер» форматирования. Он основан на <ctime> и <iomanip>. Да, он выглядит старомодно, но он понятный, стабильный и часто встречается в реальном коде.
Здесь просто разные «представления» времени: time_point удобно для арифметики, time_t — для совместимости, tm — для «разложить на год/месяц/день», а put_time — для печати по шаблону.
Печать прямо в std::cout
#include <chrono>
#include <ctime>
#include <iomanip>
#include <iostream>
int main() {
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::tm tm = *std::localtime(&t);
std::cout << std::put_time(&tm, "%Y-%m-%d %H:%M:%S") << '\n'; // например: 2026-01-16 14:05:31
}
Обратите внимание на важный момент: std::localtime возвращает указатель на внутренний статический объект, поэтому мы сразу копируем его в std::tm tm = *...;. Так безопаснее и предсказуемее.
Форматирование в std::string
Иногда вы хотите не печатать сразу, а собрать строку и потом вставить её куда-то: в JSON, в сообщение об ошибке, в лог.
#include <chrono>
#include <ctime>
#include <iomanip>
#include <sstream>
#include <string>
std::string format_local(std::chrono::system_clock::time_point tp) {
std::time_t t = std::chrono::system_clock::to_time_t(tp);
std::tm tm = *std::localtime(&t);
std::ostringstream out;
out << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
return out.str();
}
Да, здесь появляется ostringstream, но он уже встречался вам раньше (вы работали со stringstream как с идеей потокового парсинга/форматирования).
5. system_clock и steady_clock: что можно сохранять
Это очень частая ошибка. steady_clock идеален для измерений («сколько заняло»), но он не обязан быть привязанным к календарному времени и к эпохе «1970». Его значения могут быть стабильны только внутри запуска программы.
Поэтому правило простое: если вы хотите сохранить момент времени «на диск» или передать в другую программу, используйте system_clock или числовой формат, полученный из system_clock. steady_clock оставьте для таймеров, дедлайнов внутри одного запуска и измерения производительности.
6. Мини‑пример: время создания в приложении со списком задач
Представим, что у нас уже есть простое приложение «Список задач» (мы его постепенно развивали ранее: ввод/вывод, struct, vector, аккуратные функции печати). Сейчас мы хотим добавить задаче поле «когда создана».
Мы не строим идеальную систему планирования. Мы просто добавляем один практический слой: хранение момента времени как ms_since_epoch и печать для пользователя через format_local().
Модель задачи
#include <cstdint>
#include <string>
struct Task {
int id = 0;
std::string title;
std::int64_t created_ms_since_epoch = 0;
};
Обратите внимание: мы храним число. Это удобно сериализовать (например, в JSON как число) и удобно сортировать.
Создание новой задачи с текущим временем
#include <chrono>
#include <cstdint>
#include <string>
Task make_task(int id, const std::string& title) {
auto now = std::chrono::system_clock::now();
return Task{ id, title, to_unix_ms(now) };
}
Здесь используется to_unix_ms() из раздела выше.
Печать задачи «по‑человечески»
#include <iostream>
void print_task(const Task& t) {
auto tp = from_unix_ms(t.created_ms_since_epoch);
std::cout << "#" << t.id << " " << t.title
<< " (created: " << format_local(tp) << ")\n";
// например: #1 Buy milk (created: 2026-01-16 14:07:10)
}
Здесь хорошо видно разделение слоёв: внутри Task хранится число, а перед выводом мы восстановили time_point и отформатировали.
7. Как выбрать формат хранения
Задача не в том, чтобы вы заучили таблицу. Задача — чтобы у вас появилось «чутьё»: когда формат выбирается под задачу, а не «как получилось». Это особенно важно, когда вы сохраняете данные: менять формат потом больно, потому что придётся мигрировать старые файлы.
| Хранение | Подходит для | Пример поля | Комментарий |
|---|---|---|---|
|
события, логи, сортировка по времени, точность | |
практичный «дефолт» |
(секунды) |
простые даты/время, совместимость с C API | |
меньше точности, зато проще |
| "YYYY-MM-DD HH:MM:SS" | человек будет читать/править, конфиги | |
удобно глазами, хуже для машинной обработки |
Если вы уже используете JSON в проекте, то ms_since_epoch особенно приятен: он сохраняется как число, не требует экранирования и не зависит от локали.
Про ISO 8601 без фанатизма
Очень часто вы увидите формат вида 2026-01-16T14:07:10. Это похоже на ISO 8601. Полная версия обычно включает часовой пояс (Z для UTC или +03:00), но мы здесь не лезем в часовые пояса, чтобы не превратить лекцию в «научный триллер».
Практичная мысль: если вы пишете строку, делайте её однозначной по порядку полей. Формат YYYY-MM-DD хорош тем, что сортируется лексикографически так же, как по времени (при условии одинаковой зоны и одинакового формата).
Вы уже умеете печатать YYYY-MM-DD HH:MM:SS через put_time. Если захотите, можно заменить пробел на T:
// внутри format_local:
out << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S"); // 2026-01-16T14:07:10
8. Типичные ошибки
Ошибка №1: хранить время как int, не называя единицу.
int created = 1737040000; может быть и секундами, и миллисекундами, и вообще «секундами от старта программы». Через пару недель это превращается в загадку уровня «что автор имел в виду». Лечится двумя шагами: фиксируем единицу в названии (*_ms_since_epoch) и по возможности используем std::int64_t, потому что миллисекунды быстро становятся большими числами.
Ошибка №2: путать system_clock и steady_clock при сохранении на диск.
steady_clock нужен для измерений и дедлайнов внутри процесса, но не для «календарного» времени. Если вы сохранили значение steady_clock::now() и потом пытаетесь интерпретировать его «как дату», вы получите красивую фантастику, но плохой софт. Для хранения моментов используйте system_clock и производные от него числа.
Ошибка №3: печатать time_since_epoch().count() без приведения к явной единице.
count() возвращает число в единицах текущего типа duration. Если вы не сделали duration_cast<std::chrono::milliseconds>(...), вы не можете честно утверждать, что это «миллисекунды». Правильнее сначала привести к нужной единице, потом печатать число и подписывать " ms".
Ошибка №4: брать указатель из std::localtime() и хранить его «на потом».
std::localtime() возвращает указатель на внутренний объект, который может быть перезаписан следующим вызовом. Поэтому хороший паттерн: сразу копировать std::tm tm = *std::localtime(&t);. Это заметно снижает шанс поймать странные эффекты при повторных форматированиях.
Ошибка №5: сохранять время строкой «как получилось» и потом пытаться это парсить.
Если вы однажды записали "16/01/2026 14:07", а потом решили, что хотите секунды, или поменяли порядок на "01-16-2026", вы сами себе устроили мини-миграцию формата. В учебном проекте это просто раздражает, а в реальном — ломает обратную совместимость. Если уж храните строкой, фиксируйте один формат и не меняйте его без причины.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ