CodeGym /Corsi /C++ SELF /time_point e system_clock — «momento nel tempo»

time_point e system_clock — «momento nel tempo»

C++ SELF
Livello 66, Lezione 1
Disponibile

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?»
std::chrono::duration
Momento «Quando è successo / a quale ora è riferito?»
std::chrono::time_point

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:

  1. memorizzare un momento (per esempio, «quando è stato creato un task»),
  2. confrontare momenti (per esempio, «cosa è accaduto prima»),
  3. 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 è:

  1. numero di millisecondi → std::chrono::milliseconds{...}
  2. 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.

1
Compito
C++ SELF, livello 66, lezione 1
Bloccato
Suddivisione dell'epoca
Suddivisione dell'epoca
1
Compito
C++ SELF, livello 66, lezione 1
Bloccato
Duello di timestamp
Duello di timestamp
1
Compito
C++ SELF, livello 66, lezione 1
Bloccato
Spostamento temporale
Spostamento temporale
1
Compito
C++ SELF, livello 66, lezione 1
Bloccato
Piano dei lanci
Piano dei lanci
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION