Introdução

O tempo parece tão simples e trivial, mas ao mesmo tempo um conceito complexo e relativo. Desde muito tempo atrás o estudo sobre o tempo é realizado, na faculdade fiz um resumo do estudo de Aristóteles sobre o tempo, e até os dias de hoje as pessoas ainda debatem sobre o estudo dele e de muitos outros que vieram posteriormente.

Mas e no C++?

Neste post explicarei um pouco sbre a biblioteca STL do C++ std::chrono, num post anterior expliquei sobre o tempo no Linux.

Tempo Decorrido

No C++ 11 o std::chrono::steady_clock foi implementado e um TL;DR; será demonstrado nesta seção com um snippet pequeno de como obter o tempo decorrido.

Para mais detalhes, nas próximas seções uma explicação mais detalhada será realizada.

O código a seguir tem uma função de template since() usada para contar o tempo decorrido.

Na seção de tempo do Working Draft do Cpp, a explicação sobre a biblioteca chrono de C++ e a biblioteca ctime de C pode ser visualizada.

// .h
#include <chrono>

std::chrono::steady_clock::time_point m_testTimer;
template <
    class result_t   = std::chrono::milliseconds,
    class clock_t    = std::chrono::steady_clock,
    class duration_t = std::chrono::milliseconds
>
auto since(std::chrono::time_point<clock_t, duration_t> const& start)
{
    return std::chrono::duration_cast<result_t>(clock_t::now() - start);
}

// .cpp
m_testTimer = std::chrono::steady_clock::now();
// Do something
std::cout << "Time elapsed (ms):" << since(m_testTimer).count() << std::endl;

Quando quiser medir o tempo que o código leva para retornar a esta função, você pode resetar no final da tarefa com now(), medir no início da função e inicializar a variável no construtor com o seguinte pseudocódigo no arquivo cpp:

// .cpp
//constructor
m_testTimer = std::chrono::steady_clock::now();
// function
std::cout << "Time elapsed (ms):" << since(m_testTimer).count() << std::endl;
// Do something
m_testTimer = std::chrono::steady_clock::now();

Um exemplo é na robótica ou controle de posição, que é um controle muito rápido, diferente do de temperatura. Então o tempo de scan do processador ou de comunicação com outros sistemas pode interferir no resultado, use o pseudocódigo anterior para obter o tempo que a função leva para ser chamada.

C++11

Essencialmente, a funcionalidade de fuso horário (C++20) é baseada na funcionalidade de calendário (C++20), que são baseadas na funcionalidade chrono (C++11). Consequentemente, esta terminologia chrono básica começa com os três componentes do C++ 11, time point (ponto de tempo), duration (duração de tempo) e clock (relógio).

A biblioteca chrono foi projetada para ser capaz de lidar com o fato de que temporizadores e relógios podem ser diferentes em sistemas diferentes e melhorar a precisão com o tempo. Para evitar a introdução de um novo tipo de tempo a cada 10 anos ou mais - como aconteceu com as bibliotecas de tempo POSIX, por exemplo - o objetivo era fornecer um conceito de precisão neutra, separando a duration (duração de tempo) e o time point (ponto de tempo) de específicos clocks (relógios). Como resultado, o núcleo da biblioteca chrono consiste nos seguintes tipos ou conceitos, que servem como mecanismos abstratos para especificar e lidar com pontos e durações de tempo.

  • Um time point tem um ponto de partida, a chamada epoch (época), e uma duração de tempo adicional desde a epoch (época). Um exemplo típico é um ponto no tempo que representa “Meia-noite de Ano Novo de 2000”, que é descrito como “1.262.300.400 segundos desde 1º de janeiro de 1970” (este dia é a época do relógio do sistema UNIX e POSIX).
  • A duration (duração de tempo) é a diferença entre dois pontos no tempo. O número de tiques do relógio define a duração do tempo.
  • Um clock (relógio) tem um ponto inicial (epoch) e um tique para calcular o ponto no tempo atual.

O relógio é o objeto que define a epoch (época) de um time point (ponto de tempo). Assim, relógios diferentes têm épocas diferentes. Em geral, as operações que lidam com vários pontos no tempo, como o processamento da duração/diferença entre dois pontos no tempo, exigem o uso da mesma época/relógio. Um relógio também fornece uma função conveniente para fornecer o ponto no tempo de agora (now). Em outras palavras, o time point (ponto de tempo) é definido como uma duração antes ou depois de uma época, que é definida por um relógio.

Para mais informações leia o N2661 - A Foundation to Sleep On, esse documento fornece base e motivação para decisões importantes de projeto e é a fonte de muitas informações sobre a biblioteca .

Duration (Duração de Tempo)

A duração do tempo é um template de classe, std::chrono::duration consiste no tipo do tick Rep e na duração de um tick Period. Mais informações em time.syn.

template<
    class Rep,
    class Period = std::ratio<1>
> class duration;

O comprimento do tick é por padrão std::ratio<1>. std::ratio<1> significa um segundo e também pode ser escrito como std::ratio<1,1>. Portanto, std::ratio<60> é um minuto e std::ratio<1,1000> é um milissegundo. Quando Rep é um número de ponto flutuante, você pode usá-lo para armazenar frações de ticks de tempo.

Um exemplo de durações personalizadas:

std::chrono::duration<int>vinteSegundos(20);
std::chrono::duration<double,std::ratio<60>>meioMinuto(0.5);
std::chrono::duration<long,std::ratio<1,1000>> umMilisegundo(1);

No C++11 as durações mais importantes são definidas:

using nanoseconds  = duration<signed integer type of at least 64 bits, nano>;
using microseconds = duration<signed integer type of at least 55 bits, micro>;
using milliseconds = duration<signed integer type of at least 45 bits, milli>;
using seconds      = duration<signed integer type of at least 35 bits>;
using minutes      = duration<signed integer type of at least 29 bits, ratio<  60>>;
using hours        = duration<signed integer type of at least 23 bits, ratio<3600>>;
using days         = duration<signed integer type of at least 25 bits,
                            ratio_multiply<ratio<24>, hours::period>>;
using weeks        = duration<signed integer type of at least 22 bits,
                            ratio_multiply<ratio<7>, days::period>>;
using years        = duration<signed integer type of at least 17 bits,
                            ratio_multiply<ratio<146097, 400>, days::period>>;
using months       = duration<signed integer type of at least 20 bits,
                            ratio_divide<years::period, ratio<12>>>;

Com isso, podemos declarar as durações personalizadas do exemplo anterior, utilizando as definições padrões da biblioteca:

std::chrono::seconds      vinteSegundos(20);
std::chrono::hours        umDia(24);
std::chrono::milliseconds umMilisegundo(1);

Com o seguinte exemplo, é possível verificar o tempo decorrido desde a epoch (época) com durações personalizadas:

#include <chrono>
#include <iostream>

int main(){

  std::cout << std::fixed << std::endl;
  
  std::cout << "Tempo desde 1.1.1970:\n" << std::endl;

  auto timeNow= std::chrono::system_clock::now();
  auto duration= timeNow.time_since_epoch();
  std::cout << duration.count() << " nanoseconds " << std::endl;

  typedef std::chrono::duration<long double,std::ratio<1,1000000>> MyMicroSecondTick;
  MyMicroSecondTick micro(duration);
  std::cout << micro.count() << " microseconds" << std::endl;
  
  typedef std::chrono::duration<long double,std::ratio<1,1000>> MyMilliSecondTick;
  MyMilliSecondTick milli(duration);
  std::cout << milli.count() << " milliseconds" << std::endl;
  
  typedef std::chrono::duration<long double> MySecondTick;
  MySecondTick sec(duration);
  std::cout << sec.count() << " seconds " << std::endl;
  
  typedef std::chrono::duration<double, std::ratio<60>> MyMinuteTick;
  MyMinuteTick myMinute(duration);
  std::cout << myMinute.count() << " minutes" << std::endl;

  typedef std::chrono::duration<double, std::ratio<60*60>> MyHourTick;
  MyHourTick myHour(duration);
  std::cout << myHour.count() << " hours" << std::endl;
  
  typedef std::chrono::duration<double, std::ratio<60*60*24*365>> MyYearTick;
  MyYearTick myYear(duration);
  std::cout << myYear.count() << " years" << std::endl;

  typedef std::chrono::duration<double, std::ratio<60*45>> MyLessonTick;
  MyLessonTick myLesson(duration);
  std::cout << myLesson.count() << " lessons" << std::endl;

  std::cout << std::endl;
}

Operações Aritméticas

No time.duration.nonmember, os seguintes operadores podem ser usados:

template<class Rep1, class Period1, class Rep2, class Period2>
  constexpr common_type_t<duration<Rep1, Period1>, duration<Rep2, Period2>>
    operator+(const duration<Rep1, Period1>& lhs, const duration<Rep2, Period2>& rhs);

Returns: CD(CD(lhs).count() + CD(rhs).count()).

template<class Rep1, class Period1, class Rep2, class Period2>
  constexpr common_type_t<duration<Rep1, Period1>, duration<Rep2, Period2>>
  operator-(const duration<Rep1, Period1>& lhs, const duration<Rep2, Period2>& rhs);

Returns: CD(CD(lhs).count() - CD(rhs).count()).

template<class Rep1, class Period, class Rep2>
  constexpr duration<common_type_t<Rep1, Rep2>, Period>
    operator*(const duration<Rep1, Period>& d, const Rep2& s);

Constraints: is_convertible_v<const Rep2&, common_type_t<Rep1, Rep2>> is true.

Returns: CD(CD(d).count() * s).

template<class Rep1, class Rep2, class Period>
  constexpr duration<common_type_t<Rep1, Rep2>, Period>
    operator*(const Rep1& s, const duration<Rep2, Period>& d);

Constraints: is_convertible_v<const Rep1&, common_type_t<Rep1, Rep2>> is true.

Returns: d * s.

template<class Rep1, class Period, class Rep2>
  constexpr duration<common_type_t<Rep1, Rep2>, Period>
    operator/(const duration<Rep1, Period>& d, const Rep2& s);

Constraints: is_convertible_v<const Rep2&, common_type_t<Rep1, Rep2>> is true and Rep2 is not a specialization of duration.

Returns: CD(CD(d).count() / s).

template<class Rep1, class Period1, class Rep2, class Period2>
  constexpr common_type_t<Rep1, Rep2>
    operator/(const duration<Rep1, Period1>& lhs, const duration<Rep2, Period2>& rhs);

Let CD be common_type_t<duration<Rep1, Period1>, duration<Rep2, Period2>>.

Returns: CD(lhs).count() / CD(rhs).count().

template<class Rep1, class Period, class Rep2>
  constexpr duration<common_type_t<Rep1, Rep2>, Period>
    operator%(const duration<Rep1, Period>& d, const Rep2& s);

Constraints: is_convertible_v<const Rep2&, common_type_t<Rep1, Rep2>> is true and Rep2 is not a specialization of duration.

Returns: CD(CD(d).count() % s).

template<class Rep1, class Period1, class Rep2, class Period2>
  constexpr common_type_t<duration<Rep1, Period1>, duration<Rep2, Period2>>
    operator%(const duration<Rep1, Period1>& lhs, const duration<Rep2, Period2>& rhs);

Returns: CD(CD(lhs).count() % CD(rhs).count()).

Ou os operadores de comparação:

template<class Rep1, class Period1, class Rep2, class Period2>
  constexpr bool operator==(const duration<Rep1, Period1>& lhs,
                            const duration<Rep2, Period2>& rhs);

Returns: CT(lhs).count() == CT(rhs).count().

template<class Rep1, class Period1, class Rep2, class Period2>
  constexpr bool operator<(const duration<Rep1, Period1>& lhs,
                           const duration<Rep2, Period2>& rhs);

Returns: CT(lhs).count() < CT(rhs).count().

template<class Rep1, class Period1, class Rep2, class Period2>
  constexpr bool operator>(const duration<Rep1, Period1>& lhs,
                           const duration<Rep2, Period2>& rhs);

Returns: rhs < lhs.

template<class Rep1, class Period1, class Rep2, class Period2>
  constexpr bool operator<=(const duration<Rep1, Period1>& lhs,
                            const duration<Rep2, Period2>& rhs);

Returns: !(rhs < lhs).

template<class Rep1, class Period1, class Rep2, class Period2>
  constexpr bool operator>=(const duration<Rep1, Period1>& lhs,
                            const duration<Rep2, Period2>& rhs);

Returns: !(lhs < rhs).

template<class Rep1, class Period1, class Rep2, class Period2>
  requires three_way_comparable<typename CT::rep>
  constexpr auto operator<=>(const duration<Rep1, Period1>& lhs,
                             const duration<Rep2, Period2>& rhs);

Returns: CT(lhs).count() <=> CT(rhs).count().

O ponto importante aqui é que o tipo de unidade de duas durações envolvidas em tal operação pode ser diferente. Devido a uma sobrecarga fornecida de common_type<> para durações, a duração resultante terá uma unidade que é o máximo divisor comum das unidades de ambos os operandos.

Então no seguinte exemplo, d1 - d2 retorna 41990 ticks e não 41.99 ticks. Se desejar um número com ponto flutuante, as definições padrões não podem ser utilizadas, pois declaram como int, mas uma personalizada com o tipo long double deve ser usada.

#include <chrono>
#include <iostream>

typedef std::chrono::duration<long double> MySecondTick;

int main(){
    
MySecondTick sec(0);
    
std::chrono::seconds       d1(42);   // 42 seconds
std::chrono::milliseconds  d2(10);   // 10 milliseconds

sec = d1 - d2;
std::cout << (d1 - d2).count() << std::endl;
std::cout << sec.count() << std::endl;
}

Como vimos, conversões implícitas para um tipo de unidade mais preciso são sempre possíveis. No entanto, as conversões para um tipo de unidade mais grosseiro não o são, pois você pode perder informações. Por exemplo, ao converter um valor integral de 42.010 milissegundos em segundos, o valor integral resultante, 42, significa que a precisão de ter uma duração de 10 milissegundos em 42 segundos é perdida. Mas você ainda pode forçar explicitamente tal conversão com uma duração_cast. Por exemplo:

std::chrono::seconds sec(55);
std::chrono::minutes m1 = sec;      // ERRO
std::chrono::minutes m2 =
std::chrono::duration_cast<std::chrono::minutes>(sec);  // OK

Como outro exemplo, a conversão de uma duração com um tipo de ponto flutuante também requer uma conversão explícita para convertê-la em um tipo de duração integral:

std::chrono::duration<double,std::ratio<60>> halfMin(0.5);
std::chrono::seconds s1 = halfMin;   // ERROR
std::chrono::seconds s2 =
std::chrono::duration_cast<std::chrono::seconds>(halfMin);  // OK

Um exemplo típico é o código que segmenta uma duração em unidades diferentes. Por exemplo, o código a seguir segmenta uma duração de milissegundos nas horas, minutos, segundos e milissegundos correspondentes:

#include <chrono>
#include <iomanip>
#include <iostream>

using namespace std::chrono;
using namespace std;

milliseconds ms(7255042);

int main(){


hours   hh        = duration_cast<hours>(ms);
minutes mm        = duration_cast<minutes>(ms % chrono::hours(1));
seconds ss        = duration_cast<seconds>(ms % chrono::minutes(1));
milliseconds msec = duration_cast<milliseconds>(ms % chrono::seconds(1));

// and print durations and values:
std::cout << "raw: " << hh.count() << "::" << mm.count() << "::"
                << ss.count() << "::" << msec.count() << std::endl;
std::cout << "     " << setfill('0') << setw(2) << hh.count() << "::"
                                << setw(2) << mm.count() << "::"
                                << setw(2) << ss.count() << "::"
                                << setw(3) << msec.count() << std::endl;
}

A classe também fornece três funções estáticas: zero(), que produz uma duração de 0 segundos, bem como min() e max(), que produzem o valor mínimo e máximo que uma duração pode ter.

Relógios (Clocks)

Um relógio consiste em um ponto de partida e um tique de tempo. Por exemplo, um relógio pode marcar em milissegundos desde a época do UNIX (1º de janeiro de 1970) ou em nanossegundos desde o início do programa. Além disso, um relógio fornece um tipo para qualquer ponto de tempo especificado de acordo com esse relógio.

O C++ oferece três relógios, que podem ser vistos em system.clock:

  • std::system_clock representa pontos de tempo associados ao relógio de tempo real usual do sistema atual. Este relógio também fornece funções convenientes to_time_t() e from_time_t() para converter entre qualquer ponto no tempo e o tipo de hora do sistema C time_t, o que significa que você pode converter de e para horários do calendário.
  • std::steady_clock dá a garantia de que nunca será ajustado. Assim, os valores dos pontos no tempo nunca diminuem à medida que o tempo físico avança e avançam a uma taxa constante em relação ao tempo real.
  • std::high_resolution_clock representa um relógio com o período de tick mais curto possível no sistema atual.

Observe que o padrão não fornece requisitos para a precisão, a epoch e o intervalo (pontos de tempo mínimo e máximo) desses relógios. Normalmente, o ponto de partida de std::chrono:system_clock é 1.1.1970, a chamada era UNIX. Para std::chrono::steady_clock, normalmente o tempo de inicialização do seu PC.

Precisão e Estabilidade

É interessante saber quais relógios são estáveis ​​e qual precisão eles fornecem. Você obtém as respostas pelas propriedades dos relógios.

#include <chrono>
#include <iomanip>
#include <iostream>

using namespace std;

template <typename C>
void printClockData ()
{
    cout << "- precisão: ";
    // if time unit is less or equal one millisecond
    typedef typename C::period P;// type of time unit
    if (ratio_less_equal<P,milli>::value) {
       // convert to and print as milliseconds
       typedef typename ratio_multiply<P,kilo>::type TT;
       cout << fixed << double(TT::num)/TT::den
            << " milliseconds" << endl;
    }
    else {
        // print as seconds
        cout << fixed << double(P::num)/P::den << " seconds" << endl;
    }
    cout << "- estável: " << boolalpha << C::is_steady << endl;
}

int main(){
    std::cout << "system_clock: " << std::endl;
    printClockData<std::chrono::system_clock>();
    std::cout << "\nhigh_resolution_clock: " << std::endl;
    printClockData<std::chrono::high_resolution_clock>();
    std::cout << "\nsteady_clock: " << std::endl;
    printClockData<std::chrono::steady_clock>();
}

Resultado:

system_clock: 
- precisão: 0.000001 milliseconds
- estável: false

high_resolution_clock: 
- precisão: 0.000001 milliseconds
- estável: false

steady_clock: 
- precisão: 0.000001 milliseconds
- estável: true

Um relógio estável é um relógio monotônico, o que significa que o tempo só avança, nunca vai para trás, como um cronômetro. Ou seja, o valor retornado por uma chamada para now() é sempre menor ou igual ao valor retornado pela próxima chamada para now().

Isso significa que você pode usar esse relógio para mediar o tempo antes de um evento, o tempo após um evento e subtraí-los confiavelmente para obter a duração do evento porque o relógio não será ajustado durante esse tempo.

Time Point (Ponto de Tempo)

Um ponto de tempo representa um ponto específico no tempo, associando uma duração positiva ou negativa a um determinado relógio.

O template da classe time_point é a seguinte:

namespace std::chrono {
  template<class Clock, class Duration = typename Clock::duration>
  class time_point {
  public:
    using clock    = Clock;
    using duration = Duration;
    using rep      = typename duration::rep;
    using period   = typename duration::period;

  private:
    duration d_;                                                // exposition only

  public:
    // [time.point.cons], construct
    constexpr time_point();                                     // has value epoch
    constexpr explicit time_point(const duration& d);           // same as time_point() + d
    template<class Duration2>
      constexpr time_point(const time_point<clock, Duration2>& t);

    // [time.point.observer], observer
    constexpr duration time_since_epoch() const;

    // [time.point.arithmetic], arithmetic
    constexpr time_point& operator++();
    constexpr time_point operator++(int);
    constexpr time_point& operator--();
    constexpr time_point operator--(int);
    constexpr time_point& operator+=(const duration& d);
    constexpr time_point& operator-=(const duration& d);

    // [time.point.special], special values
    static constexpr time_point min() noexcept;
    static constexpr time_point max() noexcept;
  };
}

Note no trecho a seguir, que é necessário um relógio com duração ou de um relógio e uma duração.

namespace std {
   namespace chrono {
     template <typename Clock,
               typename Duration = typename Clock::duration>
         class time_point;
   }
}

O programa a seguir atribui esses pontos de tempo a tp e os imprime convertidos em uma notação de calendário:

#include <chrono>
#include <ctime>
#include <string>
#include <iostream>

std::string asString (const std::chrono::system_clock::time_point& tp)
{
     // convert to system time:
     std::time_t t = std::chrono::system_clock::to_time_t(tp);
     std::string ts = std::ctime(&t);// convert to calendar time
     ts.resize(ts.size()-1);         // skip trailing newline
     return ts;
}

int main()
{
 
     // print the epoch of this system clock:
     std::chrono::system_clock::time_point tp;
     std::cout << "epoch: " << asString(tp) << std::endl;

     // print current time:
     tp = std::chrono::system_clock::now();
     std::cout << "now:   " << asString(tp) << std::endl;

     // print minimum time of this system clock:
     tp = std::chrono::system_clock::time_point::min();
     std::cout << "min:   " << asString(tp) << std::endl;

     // print maximum time of this system clock:
     tp = std::chrono::system_clock::time_point::max();
     std::cout << "max:   " << asString(tp) << std::endl;
}

C++20

No C++20 os seguintes relógios foram adicionados:

  • utc_clock
  • tai_clock
  • gps_clock
  • file_clock
  • local_t

Os relógios std::chrono::steady_clock e std::chrono::file_clock têm uma epoch especificada na implementação. As epochs de std::chrono::system_clock, std::chrono::gps_clock, std::chrono::tai_clock e std::chrono::utc_clock começam às 00:00:00. std::chrono::file_clock é o relógio para entradas do sistema de arquivos.

Além disso, o C++ 11 oferece suporte a std::chrono::high_resolution_clock.

clock_cast

Com o std::chrono::clock_cast, você pode converter pontos de tempo entre os relógios com uma epoch. Estes são os relógios std::chrono::system_clock, std::chrono::utc_clock, std::chrono::gps_clock e std::chrono::tai_clock. Além disso, std::chrono::file_clock suporta conversão.

#include <iostream>
#include <sstream>
#include <chrono>


int main() {

  std::cout << '\n';

  using namespace std::literals;

  auto timePoint = std::chrono::system_clock::now();

  auto timePointUTC = std::chrono::clock_cast<std::chrono::utc_clock>(timePoint);
  std::cout << "UTC_time:  " << std::format("{:%F %X %Z}", timePointUTC) << '\n';

  auto timePointSys = std::chrono::clock_cast<std::chrono::system_clock>(timePoint);
  std::cout << "sys_time:  " << std::format("{:%F %X %Z}", timePointSys) << '\n';

  auto timePointFile = std::chrono::clock_cast<std::chrono::file_clock>(timePoint);
  std::cout << "file_time: " << std::format("{:%F %X %Z}", timePointFile) << '\n';

  auto timePointGPS = std::chrono::clock_cast<std::chrono::gps_clock>(timePoint);
  std::cout << "GPS_time:  " << std::format("{:%F %X %Z}", timePointGPS) << '\n';

  auto timePointTAI = std::chrono::clock_cast<std::chrono::tai_clock>(timePoint);
  std::cout << "TAI_time:  " << std::format("{:%F %X %Z}", timePointTAI) << '\n';

  std::cout << '\n';

}

std::chrono::parse

A função std::chrono::parse analisa o objeto chrono de um fluxo IO. Nas linhas a seguir, std::chrono::clock_cast converte o timePoint no relógio especificado. A linha a seguir exibe o ponto no tempo, especificando sua data (%F), sua representação de hora local (%X) e sua abreviação de fuso horário (%Z).

std::istringstream{"2024-1-1 21:00:00"} >> std::chrono::parse("%F %T"s, timePoint);

Essa função ainda não foi implementada na maioria dos compiladores, e o gcc 14 ainda será lançado com o suporte a esta versão, o MSVC já possui suporte.

Referência