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 |
|---|---|---|
|
nanosekundy | b. dokładne pomiary (ale często zaszumione) |
|
mikrosekundy | pomiary szybkich operacji |
|
milisekundy | timery UI/sieci/opóźnienia |
|
sekundy | czytelne dla człowieka interwały |
|
minuty | przypomnienia/plany/okresy |
|
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.
GO TO FULL VERSION