1. Завод по производству багов
Если вы когда-нибудь видели строку вроде timeout = 5000;, то вы уже знакомы с интригой: 5000 чего? Миллисекунд? Секунд? Микросекунд? А может это вообще «5 секунд», но кто-то просто умножил на * 1000 «на глазок», а потом забыл. Время — особенно коварная область: ошибки в единицах измерения выглядят правдоподобно и иногда «почти работают», что делает их идеальными кандидатами на ночные дебаг‑сессии.
Представьте, что вы развиваете наше учебное консольное приложение StudyPlanner (условный планировщик учебных задач). В конфиге есть «период автосохранения». Если в одном месте программы вы думаете, что число — в секундах, а в другом — что в миллисекундах, приложение будет либо сохранять всё слишком часто (и раздражать), либо слишком редко (и терять данные). И оба варианта будут «логичными», пока вы не поймаете несчастный багрепорт.
Именно поэтому в стандартной библиотеке есть std::chrono::duration: тип, в котором единица измерения зашита прямо в тип, и компилятор начинает помогать вам, а не молча наблюдать за вашей болью.
2. std::chrono::duration: «число + единица»
Когда вы впервые видите std::chrono::milliseconds{250}, ощущение примерно такое, будто C++ внезапно стал заботливым. Но это не магия и не «фреймворк времени», а довольно простая идея: duration — это пара «сколько» + «в чём измеряем». Причём «в чём измеряем» живёт не в комментарии, а в самом типе, который компилятор видит и проверяет.
Начнём с самого простого: подключаем заголовок и создаём несколько длительностей.
#include <chrono>
int main() {
std::chrono::milliseconds t1{250};
std::chrono::seconds t2{2};
(void)t1;
(void)t2;
}
Здесь самое ценное — читаемость: даже без комментариев видно, где миллисекунды, а где секунды. На таком фундаменте гораздо проще строить надёжный код.
Внутри (на уровне шаблонов) duration выглядит примерно как duration<Rep, Period>, где Rep — тип числа (например, long long), а Period — «шаг» в виде дроби (std::ratio). Но пока нам это знать не обязательно — достаточно понимать «тип хранит единицу».
Готовые единицы времени
В реальной жизни мы редко хотим оперировать «произвольными ratio». Нам нужны привычные единицы: секунды, миллисекунды, минуты, часы. В std::chrono для этого есть готовые алиасы. И это очень удобно: вы не изобретаете «ms» руками и не пишете свои константы вроде const int MS = 1000;
Ниже — маленькая табличка, чтобы у вас в голове закрепилась «линейка» времени:
| Тип длительности | Означает | Хорош для |
|---|---|---|
|
наносекунды | очень точные измерения (но часто шумные) |
|
микросекунды | измерения быстрых операций |
|
миллисекунды | таймауты UI/сети/ожиданий |
|
секунды | человеко‑понятные интервалы |
|
минуты | напоминания/планы/периоды |
|
часы | расписания/длинные интервалы |
В нашем StudyPlanner логично хранить, например, «напоминать каждые 25 минут» как std::chrono::minutes, а «минимальная пауза между автосохранениями» как std::chrono::milliseconds.
Создание длительности: почему {} — ваш друг
Когда вы пишете milliseconds{250}, вы делаете две вещи одновременно: создаёте значение и документируете единицу измерения. Это почти как назвать переменную autosave_interval_ms, только лучше: компилятор реально проверяет, что вы не перепутали типы.
Вот пример из жизни планировщика: у нас есть «интервал автосохранения» и «интервал напоминания».
#include <chrono>
struct Config {
std::chrono::milliseconds autosaveInterval{1500};
std::chrono::minutes reminderEvery{25};
};
int main() {
Config cfg{};
(void)cfg;
}
Заметьте приятный момент: поля инициализируются сразу «правильными» типами, и дальше по программе почти не нужно думать о единицах. Мы в прошлых лекциях уже обсуждали ценность сильных типов и контрактов; duration — это такой же контракт, только про время.
3. Арифметика длительностей без .count()
Когда вы начинаете работать с duration, очень хочется «достать число» через .count() и дальше делать арифметику как раньше. Но смысл chrono как раз в том, чтобы как можно дольше не выходить из мира типобезопасных длительностей.
Сложение и сравнение работают напрямую:
#include <chrono>
#include <iostream>
int main() {
auto total = std::chrono::seconds{2} + std::chrono::milliseconds{500};
if (total > std::chrono::seconds{2}) {
std::cout << "Longer than 2 seconds\n"; // Longer than 2 seconds
}
}
Здесь важна мысль: total — это всё ещё длительность, а не int. Компилятор выберет подходящий «общий тип» длительности (обычно более точную единицу), чтобы не потерять информацию.
Если вам нужно проверить «больше ли, чем 1 секунда», делайте так:
#include <chrono>
#include <iostream>
int main() {
std::chrono::milliseconds t{1500};
if (t > std::chrono::seconds{1}) {
std::cout << "More than 1 second\n"; // More than 1 second
}
}
Такой код читается как нормальный человеческий текст. А главное — вы не можете случайно сравнить «1500 миллисекунд» с «1» (непонятно чего), потому что вы сравниваете длительность с длительностью.
4. .count() — только на «границах»
Метод .count() возвращает «сырое число». И вот тут начинается ловушка: число возвращается в единицах текущего типа. То есть у milliseconds{1500}.count() будет 1500, а у seconds{1500}.count() — тоже 1500, но смысл радикально другой.
Поэтому хорошее правило стиля такое: .count() используем на границе, когда нам нужно либо вывести значение человеку, либо сохранить в файл/протокол/БД, либо передать в API, где требуется «число».
Например, печатаем интервал автосохранения строго в миллисекундах:
#include <chrono>
#include <iostream>
int main() {
std::chrono::milliseconds autosave{1500};
std::cout << autosave.count() << " ms\n"; // 1500 ms
}
Обратите внимание: суффикс " ms" — не декоративная деталь, а часть здравого смысла. Если вы печатаете просто 1500, вы создаёте будущую загадку для себя же.
5. Преобразования единиц и duration_cast
Преобразования времени — это место, где чаще всего и рождаются ошибки. И стандартная библиотека это понимает: некоторые преобразования разрешаются «легко», а некоторые требуют явного действия от программиста.
Интуитивное правило такое: перевод в более мелкие единицы обычно безопасен, потому что точность не теряется. Перевод в более крупные единицы часто означает «придётся отбросить хвост», и вот тут нужна явность.
«Без потерь»: секунды → миллисекунды
Это тот случай, где всё обычно происходит естественно: 2 секунды — это ровно 2000 миллисекунд.
#include <chrono>
#include <iostream>
int main() {
std::chrono::seconds s{2};
std::chrono::milliseconds ms = s;
std::cout << ms.count() << " ms\n"; // 2000 ms
}
Почему это удобно? Потому что вы можете задать в конфиге seconds{2}, а функция внутри системы работать в миллисекундах — и вам не нужно руками умножать на 1000 (и потом ещё раз думать «а точно на 1000?»).
«С потерей точности»: миллисекунды → секунды через duration_cast
Если у нас 1500ms, это 1.5s. Но тип std::chrono::seconds хранит целые секунды, значит нужно решить судьбу дробной части. В std::chrono базовая политика — усечение (truncation), а не округление.
#include <chrono>
#include <iostream>
int main() {
std::chrono::milliseconds ms{1500};
auto s = std::chrono::duration_cast<std::chrono::seconds>(ms);
std::cout << s.count() << " s\n"; // 1 s
}
Это не «странность», а честный контракт: если вы хотите округление — вы должны явно написать округление. На базовом уровне duration_cast — это именно «перевод типа с возможным усечением».
6. Точность: где легко потерять смысл времени
Самая обидная ошибка с временем — это не «я перепутал секунды и миллисекунды» (хотя она тоже обидная), а «я потерял точность раньше, чем нужно».
Представьте: у вас есть интервал 2500ms, и вы решили хранить его как seconds, потому что «вроде нормально». Получится 2 секунды. А если это интервал между напоминаниями, пользователь будет каждый раз получать напоминание чуть раньше, чем ожидал. По отдельности мелочь, а в сумме — «планировщик живёт своей жизнью».
Поэтому практический совет такой: внутри логики держите достаточно точную единицу, а приводите к нужной единице только на границе (вывод/хранение).
В StudyPlanner можно принять такое решение: внутри системы все «короткие» интервалы живут в миллисекундах, а «человеческие» интервалы (например, длительность учебного блока) — в минутах. Главное — чтобы это было осознанно.
Дробные длительности: std::chrono::duration<double>
Иногда дробь нужна по смыслу. Например, «1.5 секунды» — это нормальный интервал. Или «0.1 секунды». В таком случае вы можете использовать duration<double>.
Важно понимать, что дробные длительности — это не «всегда лучше». Это инструмент, который легко размазывает точность и вводит вопросы округления в неожиданных местах. Поэтому по умолчанию лучше жить в целых миллисекундах/микросекундах, а дроби вводить только когда действительно нужно.
Пример: есть 1.9s, хотим получить миллисекунды.
#include <chrono>
#include <iostream>
int main() {
std::chrono::duration<double> sec{1.9};
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(sec);
std::cout << ms.count() << " ms\n"; // 1900 ms
}
А теперь обратный (опасный) сценарий: дробные секунды в целые секунды — дробь пропадёт.
#include <chrono>
#include <iostream>
int main() {
std::chrono::duration<double> sec{1.9};
auto s = std::chrono::duration_cast<std::chrono::seconds>(sec);
std::cout << s.count() << " s\n"; // 1 s
}
Это снова усечение. Если вам по смыслу нужна точность до десятых секунды — не приводите к seconds, либо храните не в seconds, а в milliseconds.
7. Мини-встраивание в приложение: таймаут как тип
Очень легко почувствовать пользу duration, когда вы пишете API функций. Если функция принимает int timeout, то вызывающий обязан помнить единицу. А если функция принимает std::chrono::milliseconds timeout, единица становится частью контракта.
Сделаем маленький фрагмент StudyPlanner: функция «установить интервал автосохранения» (пока просто печать, без реального I/O и без ожиданий — мы сегодня не делаем паузы выполнения).
#include <chrono>
#include <iostream>
void set_autosave_interval(std::chrono::milliseconds interval) {
std::cout << "Autosave interval: " << interval.count() << " ms\n";
}
int main() {
set_autosave_interval(std::chrono::milliseconds{250}); // Autosave interval: 250 ms
set_autosave_interval(std::chrono::seconds{2}); // Autosave interval: 2000 ms
}
Обратите внимание на второй вызов: мы передали seconds{2}, а внутри всё равно получили миллисекунды. То есть вызывающий код может думать «секундами», а внутренний — работать «миллисекундами», и нигде не появляется магическое *1000.
Небольшая схема: как работать со временем в коде
Чтобы картинка сложилась, полезно держать в голове простой «конвейер»:
flowchart LR
A["Внутренняя логика
duration-тип (ms/min)"] --> B["Арифметика и сравнения
duration vs duration"]
B --> C["Граница: вывод/хранение"]
C --> D["Явное приведение единицы
duration_cast"]
D --> E["count() + суффикс
' ms' / \" s\""]
Смысл схемы такой: как можно дольше живём в duration, и только перед выводом/сохранением делаем явный duration_cast и .count().
8. Типичные ошибки при работе с std::chrono::duration
Ошибка №1: хранить интервалы как int и надеяться на «договорённость в команде».
Это классика: int timeout = 5000; и где-то в комментарии написано «ms». Потом комментарий устаревает, код копируется в другое место, а новый разработчик считает, что это секунды. Правильный подход — хранить std::chrono::milliseconds{5000} или std::chrono::seconds{5} и не оставлять компилятор без работы.
Ошибка №2: слишком рано звать .count() и делать арифметику на «сыром числе».
Когда вы достали count(), вы выкинули из значения единицу измерения. Дальше компилятор уже не может помочь: вы можете сложить «миллисекунды» с «секундами» и получить бессмысленное число. Лучше складывать duration с duration, а .count() оставлять для вывода и сериализации.
Ошибка №3: печатать count() без подписи единицы.
1500 на экране — это загадка, а не лог. Даже если вам «очевидно», что это миллисекунды, через неделю будет уже не так очевидно. Печатайте " ms", " s", " min" — и внезапно ваши логи начнут приносить радость вместо философских вопросов.
Ошибка №4: ожидать округления от duration_cast.
duration_cast в базовом виде делает усечение. Если вам нужно округление, придётся реализовать его явно (обычно через работу в более мелкой единице и ручную формулу округления). Если вы просто надеетесь, что 1500ms станет 2s, вы получите 1s — и это не баг chrono, это ваш баг ожиданий.
Ошибка №5: переводить в крупные единицы «для красоты» и терять смысл.
Иногда хочется хранить всё в seconds, потому что «красиво». Но если у вас таймауты по 250ms или 1500ms, перевод в секунды портит смысл и логику. Храните в достаточно точной единице, а для красоты вывода делайте приведение перед печатью.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ