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). Але поки що нам не обовʼязково це знати — достатньо розуміти: «тип зберігає одиницю».

Готові одиниці часу

У реальному житті ми рідко хочемо оперувати довільними співвідношеннями. Зазвичай потрібні звичні одиниці: секунди, мілісекунди, хвилини, години. У 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, перетворення на секунди псує сенс і логіку. Зберігайте значення в достатньо точній одиниці, а для зручного виведення робіть приведення перед друком.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ