CodeGym /Kursy /C++ SELF /std::chrono::duration — czas

std::chrono::duration — czas

C++ SELF
Poziom 66, Lekcja 0
Dostępny

1. Fabryka błędów

Jeśli kiedykolwiek widzieliście linijkę typu timeout = 5000;, to już znacie problem: 5000 czego? Milisekund? Sekund? Mikrosekund? A może to w ogóle „5 sekund”, ale ktoś po prostu pomnożył przez * 1000 „na oko” i potem zapomniał. Czas to szczególnie zdradliwa dziedzina: błędy w jednostkach wyglądają wiarygodnie i czasem „prawie działają”, co czyni je idealnymi kandydatami na nocne sesje debugowania.

Wyobraźcie sobie, że rozwijacie naszą konsolową aplikację szkoleniową StudyPlanner (przykładowy planer zadań). W konfiguracji jest „okres autosave”. Jeśli w jednym miejscu programu myślicie, że liczba jest w sekundach, a w innym — że w milisekundach, aplikacja będzie albo zapisywać zbyt często (i irytować), albo zbyt rzadko (i tracić dane). I oba warianty będą „sensowne”, dopóki nie otrzymacie zgłoszenia o błędzie.

Dlatego w bibliotece standardowej istnieje std::chrono::duration: typ, w którym jednostka miary jest zakodowana w typie, a kompilator zaczyna pomagać, zamiast milcząco patrzeć na wasze cierpienie.

2. std::chrono::duration: „liczba + jednostka”

Kiedy po raz pierwszy widzicie std::chrono::milliseconds{250}, odczucie jest takie, jakby C++ nagle stał się opiekuńczy. To jednak nie magia ani „framework czasu”, a prosta idea: duration to para „ile” + „w czym mierzymy”. A „w czym mierzymy” żyje nie w komentarzu, lecz w samym typie, który kompilator widzi i sprawdza.

Zacznijmy od najprostszych rzeczy: dołączamy nagłówek i tworzymy kilka czasów trwania.

#include <chrono>

int main() {
    std::chrono::milliseconds t1{250};
    std::chrono::seconds t2{2};

    (void)t1;
    (void)t2;
}

Tu najcenniejsze jest czytelność: nawet bez komentarzy widać, gdzie są milisekundy, a gdzie sekundy. Na takim fundamencie łatwiej budować niezawodny kod.

Wewnątrz (na poziomie szablonów) duration wygląda mniej więcej jak duration<Rep, Period>, gdzie Rep to typ liczby (np. long long), a Period to „krok” w postaci ułamka (std::ratio). Ale na razie nie musimy tego szczegółowo znać — wystarczy rozumieć, że „typ przechowuje jednostkę”.

Gotowe jednostki czasu

W praktyce rzadko operujemy na dowolnych ratio. Potrzebujemy znanych jednostek: sekund, milisekund, minut, godzin. W std::chrono są dostępne aliasy dla nich. To wygodne: nie wymyślacie „ms” na nowo i nie piszecie własnych stałych typu const int MS = 1000;

Poniżej krótka tabelka, żeby w głowie utrwalić „skale” czasu:

Typ trwania Oznacza Dobrze do
std::chrono::nanoseconds
nanosekundy b. dokładne pomiary (ale często zaszumione)
std::chrono::microseconds
mikrosekundy pomiary szybkich operacji
std::chrono::milliseconds
milisekundy timery UI/sieci/opóźnienia
std::chrono::seconds
sekundy czytelne dla człowieka interwały
std::chrono::minutes
minuty przypomnienia/plany/okresy
std::chrono::hours
godziny harmonogramy/długie interwały

W naszym StudyPlanner sensowne jest przechowywanie np. „przypomnień co 25 min” jako std::chrono::minutes, a „minimalnej przerwy między autosave” jako std::chrono::milliseconds.

Tworzenie trwania: dlaczego {} to twój przyjaciel

Kiedy piszesz milliseconds{250}, robisz dwie rzeczy naraz: tworzysz wartość i dokumentujesz jednostkę miary. To prawie jak nazwać zmienną autosave_interval_ms, tylko lepsze: kompilator naprawdę sprawdza, że nie pomieszałeś typów.

Oto przykład z życia planera: mamy „interwał autosave” i „interwał przypomnienia”.

#include <chrono>

struct Config {
    std::chrono::milliseconds autosaveInterval{1500};
    std::chrono::minutes reminderEvery{25};
};

int main() {
    Config cfg{};
    (void)cfg;
}

Zauważcie miły aspekt: pola są zainicjalizowane od razu „właściwymi” typami, i dalej w programie rzadko trzeba myśleć o jednostkach. W poprzednich wykładach omawialiśmy wartość silnych typów i kontraktów; duration to taki sam kontrakt, tylko dotyczący czasu.

3. Arytmetyka trwania bez .count()

Gdy zaczynacie pracować z duration, bardzo kusi „wyciągnąć liczbę” przez .count() i robić arytmetykę jak wcześniej. Ale sens chrono polega na tym, by jak najdłużej pozostać w świecie typowo-bezpiecznych trwań.

Dodawanie i porównywanie działają bezpośrednio:

#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
    }
}

Ważna myśl: total to dalej trwanie, a nie int. Kompilator wybierze odpowiedni „wspólny typ” trwania (zwykle bardziej dokładną jednostkę), aby nie utracić informacji.

Jeśli trzeba sprawdzić „czy więcej niż 1 sekunda”, róbcie to tak:

#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
    }
}

Taki kod czyta się jak zwykły tekst. I co najważniejsze — nie można przez przypadek porównać „1500 milisekund” z „1” (czegoś), bo porównujecie trwanie z trwaniem.

4. .count() — tylko na granicach

Metoda .count() zwraca „surową liczbę”. I tu zaczyna się pułapka: liczba zwracana jest w jednostkach bieżącego typu. Czyli milliseconds{1500}.count() da 1500, a seconds{1500}.count() też 1500, ale sens jest zupełnie inny.

Dobre zalecenie stylu: .count() używaj na granicy, kiedy trzeba albo wypisać wartość użytkownikowi, albo zapisać do pliku/protokołu/BD, albo przekazać do API, które oczekuje „liczby”.

Na przykład wypisujemy interwał autosave w milisekundach:

#include <chrono>
#include <iostream>

int main() {
    std::chrono::milliseconds autosave{1500};

    std::cout << autosave.count() << " ms\n"; // 1500 ms
}

Zwróćcie uwagę: sufiks " ms" to nie ozdoba, tylko element zdrowego rozsądku. Jeśli wypiszecie tylko 1500, tworzycie przyszłą zagadkę dla siebie.

5. Konwersje jednostek i duration_cast

Konwersje czasu to miejsce, gdzie najbardziej rodzą się błędy. Biblioteka standardowa to rozumie: niektóre konwersje są dozwolone „domyślnie”, a inne wymagają jawnego działania od programisty.

Zasadniczo: konwersja do mniejszych jednostek zwykle jest bezstratna, ponieważ nie tracimy precyzji. Konwersja do większych jednostek często oznacza „odcięcie reszty”, i tu wymagana jest jawność.

„Bez strat”: sekundy → milisekundy

To przypadek, gdzie wszystko zwykle przebiega naturalnie: 2 sekundy to dokładnie 2000 milisekund.

#include <chrono>
#include <iostream>

int main() {
    std::chrono::seconds s{2};
    std::chrono::milliseconds ms = s;

    std::cout << ms.count() << " ms\n"; // 2000 ms
}

Dlaczego to wygodne? Bo możecie w konfiguracji ustawić seconds{2}, a funkcja wewnątrz systemu operować w milisekundach — i nie trzeba ręcznie mnożyć przez 1000 (i potem jeszcze raz zastanawiać się „na pewno przez 1000?”).

„Ze stratą precyzji”: milisekundy → sekundy przez duration_cast

Jeśli mamy 1500ms, to jest to 1.5s. Ale typ std::chrono::seconds przechowuje całe sekundy, więc trzeba zdecydować co z częścią ułamkową. W std::chrono domyślna polityka to — obcięcie (truncation), a nie zaokrąglenie.

#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
}

To nie jest „dziwactwo”, ale uczciwy kontrakt: jeśli chcecie zaokrąglenie — musicie je jawnie zaimplementować. Na podstawowym poziomie duration_cast to właśnie „konwersja typu z możliwym obcięciem”.

6. Precyzja: gdzie łatwo utracić sens czasu

Najbardziej frustrujący błąd związany z czasem to nie „pomyliłem sekundy z milisekundami” (choć to też), ale „straciłem precyzję wcześniej, niż trzeba”.

Wyobraźcie sobie: macie interwał 2500ms, i postanowiliście przechowywać go jako seconds, bo „wydaje się OK”. Wyjdzie 2 sekundy. A jeśli to interwał między przypomnieniami, użytkownik będzie dostawał przypomnienie trochę wcześniej, niż oczekiwał. Z osobna drobiazg, w sumie — „planner działa po swojemu”.

Praktyczna rada: wewnątrz logiki trzymaj wystarczająco dokładną jednostkę, a konwertuj do żądanej jednostki tylko na granicy (wyświetlanie/zapis).

W StudyPlanner można przyjąć regułę: wewnątrz systemu krótkie interwały trzymamy w milisekundach, a „ludzkie” interwały (np. długość bloku nauki) — w minutach. Najważniejsze, żeby było to świadome.

Ułamkowe trwania: std::chrono::duration<double>

Czasem ułamek jest potrzebny. Na przykład „1.5 sekundy” to sensowny interwał. Albo „0.1 sekundy”. W takim przypadku można użyć duration<double>.

Ważne zrozumieć, że ułamkowe trwania nie są „zawsze lepsze”. To narzędzie, które łatwo rozmazuje precyzję i wprowadza kwestie zaokrągleń w nieoczekiwanych miejscach. Dlatego domyślnie lepiej trzymać się całych milisekund/mikrosekund, a ułamki stosować tylko tam, gdzie naprawdę są potrzebne.

Przykład: mamy 1.9s, chcemy otrzymać milisekundy.

#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
}

A teraz odwrotny (niebezpieczny) scenariusz: ułamkowe sekundy do całych sekund — ułamek zniknie.

#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
}

To znowu obcięcie. Jeśli potrzebujecie precyzji do dziesiątych sekundy — nie konwertujcie do seconds, albo przechowujcie w milliseconds.

7. Mała integracja w aplikacji: timeout jako typ

Łatwo odczuć korzyść z duration, gdy projektujecie API funkcji. Jeśli funkcja przyjmuje int timeout, wywołujący musi pamiętać jednostkę. Jeśli funkcja przyjmuje std::chrono::milliseconds timeout, jednostka staje się częścią kontraktu.

Zróbmy fragment StudyPlanner: funkcja „ustaw interwał autosave” (na razie tylko wypisuje, bez realnego I/O i bez oczekiwań — dziś nie robimy pauz w wykonaniu).

#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
}

Zwróćcie uwagę na drugie wywołanie: przekazaliśmy seconds{2}, a wewnątrz i tak otrzymaliśmy milisekundy. Czyli wywołujący może myśleć „w sekundach”, a wewnętrzny kod — pracować „w milisekundach”, bez magicznego *1000.

Krótka schematyka: jak pracować z czasem w kodzie

Aby ukształtować obraz, warto mieć prosty „pipeline” w głowie:

flowchart LR
    A["Logika wewnętrzna
typ duration (ms/min)"] --> B["Arytmetyka i porównania
duration vs duration"] B --> C["Granica: wyjście/zapis"] C --> D["Jawne zmienienie jednostki
duration_cast"] D --> E["count() + sufiks
' ms' / \" s\""]

Sens schematu: jak najdłużej żyjemy w duration, a tylko przed wyświetleniem/zapisem wykonujemy jawne duration_cast i .count().

8. Typowe błędy przy pracy ze std::chrono::duration

Błąd nr 1: przechowywać interwały jako int i liczyć na „umowę w zespole”.
Klasyka: int timeout = 5000; i gdzieś w komentarzu stoi „ms”. Potem komentarz się dezaktualizuje, kod kopiowany jest w inne miejsce, a nowy deweloper myśli, że to sekundy. Poprawne podejście to przechowywać std::chrono::milliseconds{5000} lub std::chrono::seconds{5} i nie pozostawiać kompilatora bez pracy.

Błąd nr 2: zbyt wcześnie wywołać .count() i robić arytmetykę na „surowej liczbie”.
Gdy wyciągniecie count(), pozbywacie się jednostki miary. Kompilator nie może dalej pomagać: możecie zsumować „milisekundy” z „sekundami” i otrzymać bezsensowną liczbę. Lepiej sumować duration z duration, a .count() zostawić do wyjścia i serializacji.

Błąd nr 3: wypisywać count() bez oznaczenia jednostki.
1500 na ekranie to zagadka, a nie log. Nawet jeśli „oczywiste”, że to milisekundy, po tygodniu przestanie być oczywiste. Wypisujcie " ms", " s", " min" — i nagle logi zaczną cieszyć, zamiast rodzić filozoficzne pytania.

Błąd nr 4: oczekiwać zaokrąglenia od duration_cast.
duration_cast w podstawowej formie obcina. Jeśli potrzebujecie zaokrąglenia, trzeba to zaimplementować jawnie (zwykle pracując w mniejszych jednostkach i stosując formułę). Jeśli po prostu liczycie, że 1500ms stanie się 2s, otrzymacie 1s — i to nie jest błąd chrono, to błąd waszych oczekiwań.

Błąd nr 5: konwertować do większych jednostek „dla estetyki” i tracić sens.
Czasem chce się trzymać wszystkiego w seconds, bo „ładniej”. Jednak jeśli macie timeouty rzędu 250ms lub 1500ms, konwersja do sekund psuje sens i logikę. Przechowujcie w wystarczająco dokładnej jednostce, a dla estetyki konwertujcie tylko przed wyświetleniem.

1
Zadanie
C++ SELF, poziom 66, lekcja 0
Niedostępne
Timer kuchenny
Timer kuchenny
1
Zadanie
C++ SELF, poziom 66, lekcja 0
Niedostępne
Łączny czas
Łączny czas
1
Zadanie
C++ SELF, poziom 66, lekcja 0
Niedostępne
Analiza opóźnienia
Analiza opóźnienia
1
Zadanie
C++ SELF, poziom 66, lekcja 0
Niedostępne
Raport sekundowy
Raport sekundowy
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION