JavaRush /Курсы /C++ SELF /std::chrono::duration...

std::chrono::duration — время

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

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;

Ниже — маленькая табличка, чтобы у вас в голове закрепилась «линейка» времени:

Тип длительности Означает Хорош для
std::chrono::nanoseconds
наносекунды очень точные измерения (но часто шумные)
std::chrono::microseconds
микросекунды измерения быстрых операций
std::chrono::milliseconds
миллисекунды таймауты UI/сети/ожиданий
std::chrono::seconds
секунды человеко‑понятные интервалы
std::chrono::minutes
минуты напоминания/планы/периоды
std::chrono::hours
часы расписания/длинные интервалы

В нашем 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, перевод в секунды портит смысл и логику. Храните в достаточно точной единице, а для красоты вывода делайте приведение перед печатью.

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