1. Perché il «momento» è un tipo separato e non semplicemente un numero
Quando sentite per la prima volta «momento nel tempo», la mente propone una soluzione pigra: «beh è solo il numero di millisecondi, usiamo un long long». E a volte funziona. Fino a quando nel progetto non arriva il secondo sviluppatore, il terzo formato di memorizzazione, e qualcuno per caso non inizia a sommare «momenti nel tempo» ottenendo una cosa matematicamente priva di senso — come se sommaste due indirizzi civici e vi aspettaste di ottenere l'indirizzo di un negozio.
In std::chrono il momento nel tempo è modellato dal tipo time_point. Il concetto è semplice: time_point è un punto sulla scala temporale, legato a specifici «orologi» (Clock). Senza un orologio il momento è incompleto, perché orologi diversi possono avere punti di riferimento differenti e persino comportamenti diversi.
Potete memorizzare una regola breve che previene metà degli errori:
| Entità | Domanda | Tipo |
|---|---|---|
| Intervallo | «Quanto è passato / quanto aspettare?» | |
| Momento | «Quando è successo / a quale ora è riferito?» | |
system_clock: «tempo reale», ma con le sue caratteristiche
std::chrono::system_clock è l'orologio che rappresenta il «tempo calendario corrente» del sistema. In pratica è quel «adesso» che vede l'utente sullo schermo, nei log, nei timestamp dei file (in senso generale), nei messaggi tipo «creato 2 minuti fa».
Tuttavia system_clock ha una caratteristica tipicamente umana: il sistema può correggere l'orologio. Per esempio la sincronizzazione di rete, la modifica manuale dell'ora, i cambi di fuso o altre impostazioni — tutto ciò può influire sugli «orologi di calendario». Quindi system_clock è ideale per «cosa mostrare all'utente e cosa scrivere nei log come data», ma non è l'ideale per misurazioni precise di durata. Le misurazioni precise le faremo con steady_clock, mentre oggi ci concentriamo onestamente sul concetto di «momento».
Dal punto di vista tecnico system_clock è un tipo che contiene un tipo annidato time_point e un metodo statico now() che restituisce il «momento attuale».
2. time_point: ottenimento e legame con l'epoca
Come ottenere un time_point
Il primo incontro con time_point spesso sembra complicato e template-centrico. In realtà è molto semplice da usare, specialmente se si usa auto.
Ecco un esempio minimale:
#include <chrono>
#include <iostream>
int main() {
auto now = std::chrono::system_clock::now();
(void)now; // per ora solo registrato: now è un time_point
std::cout << "Got current time_point\n"; // Got current time_point
}
Il punto chiave è che now — non è un numero e nemmeno una duration. È precisamente un momento. Non è molto utile da solo (stamparlo «così com'è» è scomodo), ma è utile come mattoncino per:
- memorizzare un momento (per esempio, «quando è stato creato un task»),
- confrontare momenti (per esempio, «cosa è accaduto prima»),
- calcolare l'intervallo tra momenti (time_point - time_point).
E sì: in chrono tutto è progettato in modo che l'aritmetica e le conversioni siano il più possibile sicure a livello di tipi; lo standard si è mosso per permettere che le operazioni su duration/time_point possano essere constexpr quando sensato.
Epoch e time_since_epoch()
Spesso è necessario salvare un momento «come numero»: per esempio in un file, in un database, in JSON o semplicemente in un campo che sia facile da ordinare e confrontare.
Per questo time_point può essere convertito in una duration rispetto a una certa epoca (epoch) — il punto di riferimento scelto dall'orologio.
Si fa così:
#include <chrono>
#include <iostream>
int main() {
auto now = std::chrono::system_clock::now();
auto since_epoch = now.time_since_epoch(); // duration
std::cout << "duration count (raw) = " << since_epoch.count() << '\n';
}
Il trucco qui, ma molto comune, è che since_epoch.count() è il «numero grezzo» nelle unità del tipo duration usato da system_clock. Quel tipo e la sua risoluzione dipendono dall'implementazione (piattaforma/compilatore). Quindi stampare o salvare count() senza conversione esplicita è quasi sempre una cattiva idea.
Approccio corretto: convertire la durata dall'epoca all'unità desiderata. Di solito si scelgono millisecondi, a volte secondi.
Esempio con millisecondi:
#include <chrono>
#include <iostream>
int main() {
auto now = std::chrono::system_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()
);
long long ms_since_epoch = ms.count();
std::cout << ms_since_epoch << " ms since epoch\n";
}
Qui facciamo due cose importanti. Primo, specifichiamo esplicitamente le unità (milliseconds). Secondo, nominiamo la variabile in modo che le unità siano nel nome (ms_since_epoch). Non è pignoleria — è prevenzione contro bug del tipo «perché tutto è 1000 volte più grande».
4. Aritmetica con time_point: cosa è permesso e cosa è privo di senso
Quando in codice appare un «momento», istintivamente si tende alla classica aritmetica. Qui è importante mettersi d'accordo prima (con se stessi e con il compilatore).
Con time_point la logica è la seguente:
- time_point + duration restituisce un nuovo time_point (il momento è avanzato/indietreggiato).
- time_point - time_point restituisce una duration (quanto tempo passa tra i momenti).
- time_point + time_point è privo di senso, e la libreria standard non permette di farlo.
Guardiamo un esempio: «tra 5 secondi rispetto al momento corrente».
#include <chrono>
#include <iostream>
int main() {
auto now = std::chrono::system_clock::now();
auto later = now + std::chrono::seconds{5};
auto diff = later - now; // duration
auto s = std::chrono::duration_cast<std::chrono::seconds>(diff);
std::cout << s.count() << " s\n"; // 5 s
}
Qui il significato è chiaro: la differenza tra due momenti è un intervallo. Così otteniamo finalmente «quanto tempo è passato» partendo da «quando è successo».
Un appunto per gli amanti dell'ottimizzazione del tipo: a volte si vuole memorizzare il tempo come numero unsigned («non può essere negativo!»). In pratica questo porta spesso a sorprese sgradevoli durante sottrazioni e confronti; nelle discussioni dello standard ci sono anche bug/issue specifici relativi alla sottrazione di time_point con duration unsigned.
Regola pratica nel codice di studio: se non siete sicuri al 200%, non trasformate il tempo in unsigned. Conservate i «millisecondi dall'epoca» in un tipo intero con segno (per esempio long long o std::int64_t).
5. std::time_t: ponte verso il «vecchio mondo»
Anche se std::chrono è l'approccio moderno, il «vecchio mondo» delle API C per il tempo è ancora presente. Capita di avere funzioni che accettano/ritornano std::time_t. Di solito rappresenta i «secondi dall'epoca» (con precisione al secondo).
system_clock fornisce funzioni pronte per il ponte:
- std::chrono::system_clock::to_time_t(time_point)
- std::chrono::system_clock::from_time_t(time_t)
Esempio minimale:
#include <chrono>
#include <ctime>
#include <iostream>
int main() {
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::cout << t << " seconds since epoch\n";
}
È importante capire la limitazione: time_t normalmente memorizza secondi, quindi i millisecondi in questa conversione vengono «compressi». Per i log che indicano «in quale secondo è successo» questo spesso basta, ma per timestamp precisi (per esempio «clic su un bottone alle 12:34:56.789») non è sufficiente.
6. Esempio pratico: created_at e l'«età» di un task
Per non restare astratti, sviluppiamo una piccola applicazione console che miglioreremo nel corso. Sarà una semplice «todo list»: i task contengono testo e il momento di creazione. Per ora non salviamo nulla su file né gestiamo formati — oggi ci basta imparare a marcare il tempo e a confrontare i momenti.
Creiamo un modello Task che memorizza il momento di creazione come «millisecondi dall'epoca». Perché non usare il time_point direttamente nella struct? Si potrebbe, ma il numero è più semplice da serializzare e stampare, e non è legato a una rappresentazione interna specifica. Fissiamo consapevolmente le unità: millisecondi.
#include <string>
struct Task {
int id = 0;
std::string text;
long long created_ms = 0; // ms since epoch
};
Ora scriviamo una funzione che restituisce «adesso» in millisecondi:
#include <chrono>
long long now_ms_since_epoch() {
auto now = std::chrono::system_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()
);
return ms.count();
}
Notate che la funzione ritorna un numero, ma internamente fa tutto «nel modo corretto»: prende un time_point, lo converte in una duration dall'epoca, lo cast a millisecondi e solo dopo prende count().
Aggiungiamo la creazione di un task:
#include <iostream>
#include <vector>
int main() {
std::vector<Task> tasks;
Task t;
t.id = 1;
t.text = "Buy milk (or compile errors)";
t.created_ms = now_ms_since_epoch();
tasks.push_back(t);
std::cout << "Created task at " << t.created_ms << " ms\n";
}
L'idea è che ora ogni task ha il suo «passaporto»: il momento di creazione. Possiamo, per esempio, ordinare i task per created_ms, confrontarli, filtrarli, trovare i più recenti — tutto senza convertire manualmente.
Da numero a time_point
A volte memorizzate il momento come numero (per esempio created_ms), ma avete bisogno di convertire di nuovo a time_point per fare aritmetica cronologica corretta (per esempio confrontare con system_clock::now()).
La procedura è:
- numero di millisecondi → std::chrono::milliseconds{...}
- system_clock::time_point{milliseconds{...}}
Ecco una piccola utility:
#include <chrono>
std::chrono::system_clock::time_point tp_from_ms(long long ms_since_epoch) {
return std::chrono::system_clock::time_point{
std::chrono::milliseconds{ms_since_epoch}
};
}
Ora possiamo calcolare l'«età del task» in secondi, senza rompere i tipi:
#include <chrono>
std::chrono::seconds task_age_seconds(const Task& t) {
auto created_tp = tp_from_ms(t.created_ms);
auto now = std::chrono::system_clock::now();
auto age = now - created_tp; // duration
return std::chrono::duration_cast<std::chrono::seconds>(age);
}
C'è un'idea sottile ma importante: non stiamo «sottraendo millisecondi da millisecondi» come numeri grezzi, ma lavoriamo con i tipi chrono. Questo è particolarmente comodo quando le unità possono cambiare: oggi si stampa in secondi, domani in minuti, e la logica rimane corretta.
7. Schema: time_point, epoch e duration
Talvolta è utile vedere tutto come uno schema per non avere più paura:
flowchart LR
TP["time_point (momento)"] -->|"time_since_epoch()"| D["duration (intervallo dall'epoch)"]
D -->|duration_cast<milliseconds>| MS["milliseconds"]
MS -->|"count()"| N["numero intero (ms since epoch)"]
Il concetto: time_point è comodo per l'aritmetica dei momenti, duration è comodo per gli intervalli, il «numero» è comodo per conservare/trasmettere. Si può andare avanti e indietro, ma è meglio farlo consapevolmente e con unità fissate.
8. Errori tipici lavorando con time_point e system_clock
Errore №1: confondere time_point e duration (e cercare di trattare gli «intervalli» come momenti).
Questo errore appare quando, per esempio, memorizzate un «timeout di 500ms» come system_clock::time_point e poi non sapete cosa farne. Il timeout è un intervallo, deve stare in duration. Il momento (time_point) serve quando sapete «quando esattamente»: «ora di creazione del task», «ora dell'ultimo salvataggio», «deadline».
Errore №2: usare time_since_epoch().count() senza duration_cast e poi pensare che siano millisecondi.
Trappola classica: il codice compila, il numero viene stampato, e dopo un mese si scopre che su una macchina erano nanosecondi, su un'altra qualcos'altro. Abitudine corretta: convertire sempre all'unità esplicita (milliseconds, seconds) e poi chiamare count().
Errore №3: sommare due time_point o «aggiungere un numero» senza specificare le unità.
Sommare due momenti è come sommare «martedì» e «giovedì»: linguisticamente possibile, semanticamente privo di senso. In chrono l'operazione corretta è «momento + intervallo». Quindi se volete «tra 5 secondi», aggiungete std::chrono::seconds{5}, non + 5 (il tempo non ama essere trattato come un int).
Errore №4: memorizzare timestamp in unsigned e incontrare comportamenti strani alla sottrazione.
Sembra logico: «il tempo non è negativo». In pratica la sottrazione e i confronti possono comportarsi in modo inatteso, soprattutto se compare un intervallo negativo o diversi tipi di durata. Nella libreria standard e nelle discussioni ci sono dettagli relativi alla sottrazione di time_point con duration unsigned.
Usate un tipo con segno per i «millisecondi dall'epoca» salvo stretta necessità.
Errore №5: usare system_clock per misurare la durata di operazioni e sorprendersi di intervalli negativi.
Questo succede quasi inevitabilmente quando il sistema porta indietro l'orologio e voi misurate «quanto è durato» usando l'orologio di calendario. Oggi abbiamo solo annotato il rischio. Lo strumento giusto per le misurazioni è steady_clock, che vedremo nella prossima lezione.
GO TO FULL VERSION