C++使用chrono庫處理日期和時間的實現方法

C++11 中提供瞭日期和時間相關的庫 chrono,通過 chrono 庫可以很方便地處理日期和時間,為程序的開發提供瞭便利。chrono 庫主要包含三種類型的類:時間間隔duration、時鐘clocks、時間點time point。

1. 時間間隔 duration

1.1 常用類成員

duration表示一段時間間隔,用來記錄時間長度,可以表示幾秒、幾分鐘、幾個小時的時間間隔。duration 的原型如下:

// 定義於頭文件 <chrono>
template<
    class Rep,
    class Period = std::ratio<1>
> class duration;

ratio 類表示每個時鐘周期的秒數,其中第一個模板參數 Num 代表分子,Denom 代表分母,該分母值默認為 1,因此,ratio 代表的是一個分子除以分母的數值,比如:ratio<2> 代表一個時鐘周期是 2 秒,ratio<60 > 代表一分鐘,ratio<60*60 > 代表一個小時,ratio<60*60*24 > 代表一天。而 ratio<1,1000 > 代表的是 1/1000 秒,也就是 1 毫秒,ratio<1,1000000 > 代表一微秒,ratio<1,1000000000 > 代表一納秒。

為瞭方便使用,在標準庫中定義瞭一些常用的時間間隔,比如:時、分、秒、毫秒、微秒、納秒,它們都位於 chrono 命名空間下,定義如下:

類型 定義
納秒:std::chrono::nanoseconds duration<Rep/ 至少 64 位的有符號整數類型 /, std::nano>
微秒:std::chrono::microseconds duration<Rep/ 至少 55 位的有符號整數類型 /, std::micro>
毫秒:std::chrono::milliseconds duration<Rep/ 至少 45 位的有符號整數類型 /, std::milli>
秒:std::chrono::seconds duration<Rep/ 至少 35 位的有符號整數類型 />
分鐘:std::chrono::minutes duration<Rep/ 至少 29 位的有符號整數類型 /, std::ratio<60>>
小時:std::chrono::hours duration<Rep/ 至少 23 位的有符號整數類型 /, std::ratio<3600>>

註意:到 hours 為止的每個預定義時長類型至少涵蓋 ±292 年的范圍。

duration 類的構造函數原型如下:

// 1. 拷貝構造函數
duration( const duration& ) = default;
// 2. 通過指定時鐘周期的類型來構造對象
template< class Rep2 >
constexpr explicit duration( const Rep2& r );
// 3. 通過指定時鐘周期類型,和時鐘周期長度來構造對象
template< class Rep2, class Period2 >
constexpr duration( const duration<Rep2,Period2>& d );

為瞭更加方便的進行 duration 對象之間的操作,類內部進行瞭操作符重載:

操作符重載 描述
operator= 賦值內容 (公開成員函數)
operator+ operator- 賦值內容 (公開成員函數)
operator++ operator++(int) operator– operator–(int) 遞增或遞減周期計數 (公開成員函數)
operator+= operator-= operator*= operator/= operator%= 實現二個時長間的復合賦值 (公開成員函數)

duration 類還提供瞭獲取時間間隔的時鐘周期數的方法 count (),函數原型如下:

constexpr rep count() const;

1.2 類的使用

通過構造函數構造事件間隔對象示例代碼如下:

#include <chrono>
#include <iostream>
using namespace std;
int main()
{
    chrono::hours h(1);                          // 一小時
    chrono::milliseconds ms{ 3 };                // 3 毫秒
    chrono::duration<int, ratio<1000>> ks(3);    // 3000 秒
 
    // chrono::duration<int, ratio<1000>> d3(3.5);  // error
    chrono::duration<double> dd(6.6);               // 6.6 秒
 
    // 使用小數表示時鐘周期的次數
    chrono::duration<double, std::ratio<1, 30>> hz(3.5);
}
  • h(1) 時鐘周期為 1 小時,共有 1 個時鐘周期,所以 h 表示的時間間隔為 1 小時
  • ms(3) 時鐘周期為 1 毫秒,共有 3 個時鐘周期,所以 ms 表示的時間間隔為 3 毫秒
  • ks(3) 時鐘周期為 1000 秒,一共有三個時鐘周期,所以 ks 表示的時間間隔為 3000 秒
  • d3(3.5) 時鐘周期為 1000 秒,時鐘周期數量隻能用整形來表示,但是此處指定的是浮點數,因此語法錯誤
  • dd(6.6) 時鐘周期為默認的 1 秒,共有 6.6 個時鐘周期,所以 dd 表示的時間間隔為 6.6 秒
  • hz(3.5) 時鐘周期為 1/30 秒,共有 3.5 個時鐘周期,所以 hz 表示的時間間隔為 1/30*3.5 秒

chrono 庫中根據 duration 類封裝瞭不同長度的時鐘周期(也可以自定義),基於這個時鐘周期再進行周期次數的設置就可以得到總的時間間隔瞭(時鐘周期 * 周期次數 = 總的時間間隔)。

示例代碼如下:

#include <chrono>
#include <iostream>
int main()
{
    std::chrono::milliseconds ms{3};         // 3 毫秒
    std::chrono::microseconds us = 2*ms;     // 6000 微秒
    // 時間間隔周期為 1/30 秒
    std::chrono::duration<double, std::ratio<1, 30>> hz(3.5);
 
    std::cout <<  "3 ms duration has " << ms.count() << " ticks\n"
              <<  "6000 us duration has " << us.count() << " ticks\n"
              <<  "3.5 hz duration has " << hz.count() << " ticks\n";       
}

輸出的結果為:

3 ms duration has 3 ticks
6000 us duration has 6000 ticks
3.5 hz duration has 3.5 ticks

  • ms 時間單位為毫秒,初始化操作 ms{3} 表示時間間隔為 3 毫秒,一共有 3 個時間周期,每個周期為 1 毫秒
  • us 時間單位為微秒,初始化操作 2*ms 表示時間間隔為 6000 微秒,一共有 6000 個時間周期,每個周期為 1 微秒
  • hz 時間單位為秒,初始化操作 hz(3.5) 表示時間間隔為 1/30*3.5 秒,一共有 3.5 個時間周期,每個周期為 1/30 秒

由於在 duration 類內部做瞭操作符重載,因此時間間隔之間可以直接進行算術運算,比如我們要計算兩個時間間隔的差值,就可以在代碼中做如下處理:

#include <iostream>
#include <chrono>
using namespace std;
 
int main()
{
    chrono::minutes t1(10);
    chrono::seconds t2(60);
    chrono::seconds t3 = t1 - t2;
    cout << t3.count() << " second" << endl;
}

程序輸出的結果:

540 second

在上面的測試程序中,t1 代表 10 分鐘,t2 代表 60 秒,t3 是 t1 減去 t2,也就是 60*10-60=540,這個 540 表示的時鐘周期,每個時鐘周期是 1 秒,因此兩個時間間隔之間的差值為 540 秒。

註意事項:duration 的加減運算有一定的規則,當兩個 duration 時鐘周期不相同的時候,會先統一成一種時鐘,然後再進行算術運算,統一的規則如下:假設有 ratio<x1,y1> 和 ratio<x2,y2 > 兩個時鐘周期,首先需要求出 x1,x2 的最大公約數 X,然後求出 y1,y2 的最小公倍數 Y,統一之後的時鐘周期 ratio 為 ratio<X,Y>。

#include <iostream>
#include <chrono>
using namespace std;
 
int main()
{
    chrono::duration<double, ratio<9, 7>> d1(3);
    chrono::duration<double, ratio<6, 5>> d2(1);
    // d1 和 d2 統一之後的時鐘周期
    chrono::duration<double, ratio<3, 35>> d3 = d1 - d2;
}

對於分子 6,、9 最大公約數為 3,對於分母 7、5 最小公倍數為 35,因此推導出的時鐘周期為 ratio<3,35>

2. 時間點 time point

chrono 庫中提供瞭一個表示時間點的類 time_point,該類的定義如下:

// 定義於頭文件 <chrono>
template<
    class Clock,
    class Duration = typename Clock::duration
> class time_point;

它被實現成如同存儲一個 Duration 類型的自 Clock 的紀元起始開始的時間間隔的值,通過這個類最終可以得到時間中的某一個時間點。

  • Clock:此時間點在此時鐘上計量
  • Duration:用於計量從紀元起時間的 std::chrono::duration 類型

time_point 類的構造函數原型如下:

// 1. 構造一個以新紀元(epoch,即:1970.1.1)作為值的對象,需要和時鐘類一起使用,不能單獨使用該無參構造函數
time_point();
// 2. 構造一個對象,表示一個時間點,其中d的持續時間從epoch開始,需要和時鐘類一起使用,不能單獨使用該構造函數
explicit time_point( const duration& d );
// 3. 拷貝構造函數,構造與t相同時間點的對象,使用的時候需要指定模板參數
template< class Duration2 >
time_point( const time_point<Clock,Duration2>& t );

在這個類中除瞭構造函數還提供瞭另外一個 time_since_epoch() 函數,用來獲得 1970 年 1 月 1 日到 time_point 對象中記錄的時間經過的時間間隔(duration),函數原型如下:

duration time_since_epoch() const;

除此之外,時間點 time_point 對象和時間段對象 duration 之間還支持直接進行算術運算(即加減運算),時間點對象之間可以進行邏輯運算,具體細節可以參考下面的表格:

其中 tp 和 tp2 是 time_point 類型的對象, dtn 是 duration 類型的對象。

描述 操作 返回值
復合賦值 (成員函數) operator+= tp += dtn *this
復合賦值 (成員函數)  operator-= tp -= dtn *this
算術運算符 (非成員函數) operator+ tp + dtn a time_point value
算術運算符 (非成員函數) operator+ dtn + tp a time_point value
算術運算符 (非成員函數) operator- tp – dtn a time_point value
算術運算符 (非成員函數) operator- ttp – tp2 aduration value
關系操作符 (非成員函數) operator== tp == tp2 a bool value
關系操作符 (非成員函數) operator!= tp != tp2 a bool value
關系操作符 (非成員函數) operator< tp < tp2 a bool value
關系操作符 (非成員函數) operator> tp > tp2 a bool value
關系操作符 (非成員函數) operator>= tp >= tp2 a bool value
關系操作符 (非成員函數) operator<= tp <= tp2 a bool value

由於該時間點類經常和下面要介紹的時鐘類一起使用,所以在此先不舉例,在時鐘類的示例代碼中會涉及到時間點類的使用,到此為止隻需要搞明白時間點類的提供的這幾個函數的作用就可以瞭。

3. 時鐘 clocks

chrono 庫中提供瞭獲取當前的系統時間的時鐘類,包含的時鐘一共有三種:

  • system_clock:系統的時鐘,系統的時鐘可以修改,甚至可以網絡對時,因此使用系統時間計算時間差可能不準。
  • steady_clock:是固定的時鐘,相當於秒表。開始計時後,時間隻會增長並且不能修改,適合用於記錄程序耗時
  • high_resolution_clock:和時鐘類 steady_clock 是等價的(是它的別名)。

在這些時鐘類的內部有 time_point、duration、Rep、Period 等信息,基於這些信息來獲取當前時間,以及實現 time_t 和 time_point 之間的相互轉換。

時鐘類成員類型 描述
rep 表示時鐘周期次數的有符號算術類型
period 表示時鐘計次周期的 std::ratio 類型
duration 時間間隔,可以表示負時長
time_point 表示在當前時鐘裡邊記錄的時間點

在使用chrono提供的時鐘類的時候,不需要創建類對象,直接調用類的靜態方法就可以得到想要的時間瞭。

3.1 system_clock

具體來說,時鐘類 system_clock 是一個系統范圍的實時時鐘。system_clock 提供瞭對當前時間點 time_point 的訪問,將得到時間點轉換為 time_t 類型的時間對象,就可以基於這個時間對象獲取到當前的時間信息瞭。

system_clock 時鐘類在底層源碼中的定義如下:

struct system_clock { // wraps GetSystemTimePreciseAsFileTime/GetSystemTimeAsFileTime
    using rep                       = long long;
    using period                    = ratio<1, 10'000'000>; // 100 nanoseconds
    using duration                  = chrono::duration<rep, period>;
    using time_point                = chrono::time_point<system_clock>;
    static constexpr bool is_steady = false;
 
    _NODISCARD static time_point now() noexcept 
    { // get current time
        return time_point(duration(_Xtime_get_ticks()));
    }
 
    _NODISCARD static __time64_t to_time_t(const time_point& _Time) noexcept 
    { // convert to __time64_t
        return duration_cast<seconds>(_Time.time_since_epoch()).count();
    }
 
    _NODISCARD static time_point from_time_t(__time64_t _Tm) noexcept 
    { // convert from __time64_t
        return time_point{seconds{_Tm}};
    }
};

通過以上源碼可以瞭解到在 system_clock 類中的一些細節信息:

  • rep:時鐘周期次數是通過整形來記錄的 long long
  • period:一個時鐘周期是 100 納秒 ratio<1, 10’000’000>
  • duration:時間間隔為 rep*period 納秒 chrono::duration<rep, period>
  • time_point:時間點通過系統時鐘做瞭初始化 chrono::time_p- oint<system_clock>,裡面記錄瞭新紀元時間點

另外還可以看到 system_clock 類一共提供瞭三個靜態成員函數:

// 返回表示當前時間的時間點。
static std::chrono::time_point<std::chrono::system_clock> now() noexcept;
// 將 time_point 時間點類型轉換為 std::time_t 類型
static std::time_t to_time_t( const time_point& t ) noexcept;
// 將 std::time_t 類型轉換為 time_point 時間點類型
static std::chrono::system_clock::time_point from_time_t( std::time_t t ) noexcept;

比如,我們要獲取當前的系統時間,並且需要將其以能夠識別的方式打印出來,示例代碼如下:

#include <chrono>
#include <iostream>
using namespace std;
using namespace std::chrono;
int main()
{
    // 新紀元1970.1.1時間
    system_clock::time_point epoch;
 
    duration<int, ratio<60*60*24>> day(1);
    // 新紀元1970.1.1時間 + 1天
    system_clock::time_point ppt(day);
 
    using dday = duration<int, ratio<60 * 60 * 24>>;
    // 新紀元1970.1.1時間 + 10天
    time_point<system_clock, dday> t(dday(10));
 
    // 系統當前時間
    system_clock::time_point today = system_clock::now();
    
    // 轉換為time_t時間類型
    time_t tm = system_clock::to_time_t(today);
    cout << "今天的日期是:    " << ctime(&tm);
 
    time_t tm1 = system_clock::to_time_t(today+day);
    cout << "明天的日期是:    " << ctime(&tm1);
 
    time_t tm2 = system_clock::to_time_t(epoch);
    cout << "新紀元時間:      " << ctime(&tm2);
 
    time_t tm3 = system_clock::to_time_t(ppt);
    cout << "新紀元時間+1天:  " << ctime(&tm3);
 
    time_t tm4 = system_clock::to_time_t(t);
    cout << "新紀元時間+10天: " << ctime(&tm4);
}

示例代碼打印的結果為:

今天的日期是:    Thu Apr  8 11:09:49 2021
明天的日期是:    Fri Apr  9 11:09:49 2021
新紀元時間:      Thu Jan  1 08:00:00 1970
新紀元時間+1天:  Fri Jan  2 08:00:00 1970
新紀元時間+10天: Sun Jan 11 08:00:00 1970

3.2 steady_clock

如果我們通過時鐘不是為瞭獲取當前的系統時間,而是進行程序耗時的時長,此時使用 syetem_clock 就不合適瞭,因為這個時間可以跟隨系統的設置發生變化。在 C++11 中提供的時鐘類 steady_clock 相當於秒表,隻要啟動就會進行時間的累加,並且不能被修改,非常適合於進行耗時的統計。

steady_clock 時鐘類在底層源碼中的定義如下:

struct steady_clock { // wraps QueryPerformanceCounter
    using rep                       = long long;
    using period                    = nano;
    using duration                  = nanoseconds;
    using time_point                = chrono::time_point<steady_clock>;
    static constexpr bool is_steady = true;
 
    // get current time
    _NODISCARD static time_point now() noexcept 
    { 
        // doesn't change after system boot
        const long long _Freq = _Query_perf_frequency(); 
        const long long _Ctr  = _Query_perf_counter();
        static_assert(period::num == 1, "This assumes period::num == 1.");
        const long long _Whole = (_Ctr / _Freq) * period::den;
        const long long _Part  = (_Ctr % _Freq) * period::den / _Freq;
        return time_point(duration(_Whole + _Part));
    }
};

通過以上源碼可以瞭解到在 steady_clock 類中的一些細節信息:

  • rep:時鐘周期次數是通過整形來記錄的 long long
  • period:一個時鐘周期是 1 納秒 nano
  • duration:時間間隔為 1 納秒 nanoseconds
  • time_point:時間點通過系統時鐘做瞭初始化 chrono::time_point<steady_clock>

另外,在這個類中也提供瞭一個靜態的 now () 方法,用於得到當前的時間點,函數原型如下:

static std::chrono::time_point<std::chrono::steady_clock> now() noexcept;

假設要測試某一段程序的執行效率,可以計算它執行期間消耗的總時長,示例代碼如下:

#include <chrono>
#include <iostream>
using namespace std;
using namespace std::chrono;
int main()
{
    // 獲取開始時間點
    steady_clock::time_point start = steady_clock::now();
    // 執行業務流程
    cout << "print 1000 stars ...." << endl;
    for (int i = 0; i < 1000; ++i)
    {
        cout << "*";
    }
    cout << endl;
    // 獲取結束時間點
    steady_clock::time_point last = steady_clock::now();
    // 計算差值
    auto dt = last - start;
    cout << "總共耗時: " << dt.count() << "納秒" << endl;
}

3.3 high_resolution_clock

high_resolution_clock 提供的時鐘精度比 system_clock 要高,它也是不可以修改的。在底層源碼中,這個類其實是 steady_clock 類的別名。

using high_resolution_clock = steady_clock;
因此 high_resolution_clock 的使用方式和 steady_clock 是一樣的,在此就不再過多進行贅述瞭。

4. 轉換函數

4.1 duration_cast

duration_cast 是 chrono 庫提供的一個模板函數,這個函數不屬於 duration 類。通過這個函數可以對 duration 類對象內部的時鐘周期 Period,和周期次數的類型 Rep 進行修改,該函數原型如下:

template <class ToDuration, class Rep, class Period>
  constexpr ToDuration duration_cast (const duration<Rep,Period>& dtn);

在源周期能準確地為目標周期所整除的場合(例如小時到分鐘),浮點時長和整數時長間轉型能隱式進行無需使用 duration_cast ,其他情況下都需要通過函數進行轉換。

我們可以修改一下上面測試程序執行時間的代碼,在代碼中修改 duration 對象的屬性:

#include <iostream>
#include <chrono>
using namespace std;
using namespace std::chrono;
 
void f()
{
    cout << "print 1000 stars ...." << endl;
    for (int i = 0; i < 1000; ++i)
    {
        cout << "*";
    }
    cout << endl;
}
 
int main()
{
    auto t1 = steady_clock::now();
    f();
    auto t2 = steady_clock::now();
 
    // 整數時長:要求 duration_cast
    auto int_ms = duration_cast<chrono::milliseconds>(t2 - t1);
 
    // 小數時長:不要求 duration_cast
    duration<double, ratio<1, 1000>> fp_ms = t2 - t1;
 
    cout << "f() took " << fp_ms.count() << " ms, "
        << "or " << int_ms.count() << " whole milliseconds\n";
}

示例代碼輸出的結果:

print 1000 stars ….
****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
f() took 40.2547 ms, or 40 whole milliseconds

4.2 time_point_cast

time_point_cast 也是 chrono 庫提供的一個模板函數,這個函數不屬於 time_point 類。函數的作用是對時間點進行轉換,因為不同的時間點對象內部的時鐘周期 Period,和周期次數的類型 Rep 可能也是不同的,一般情況下它們之間可以進行隱式類型轉換,也可以通過該函數顯示的進行轉換,函數原型如下:

template <class ToDuration, class Clock, class Duration>
time_point<Clock, ToDuration> time_point_cast(const time_point<Clock, Duration> &t);

關於函數的使用,示例代碼如下:

#include <chrono>
#include <iostream>
using namespace std;
 
using Clock = chrono::high_resolution_clock;
using Ms = chrono::milliseconds;
using Sec = chrono::seconds;
template<class Duration>
using TimePoint = chrono::time_point<Clock, Duration>;
 
void print_ms(const TimePoint<Ms>& time_point)
{
    std::cout << time_point.time_since_epoch().count() << " ms\n";
}
 
int main()
{
    TimePoint<Sec> time_point_sec(Sec(6));
    // 無精度損失, 可以進行隱式類型轉換
    TimePoint<Ms> time_point_ms(time_point_sec);
    print_ms(time_point_ms);    // 6000 ms
 
    time_point_ms = TimePoint<Ms>(Ms(6789));
    // error,會損失精度,不允許進行隱式的類型轉換
    TimePoint<Sec> sec(time_point_ms);
 
    // 顯示類型轉換,會損失精度。6789 truncated to 6000
    time_point_sec = std::chrono::time_point_cast<Sec>(time_point_ms);
    print_ms(time_point_sec); // 6000 ms
}

註意事項:關於時間點的轉換如果沒有沒有精度的損失可以直接進行隱式類型轉換,如果會損失精度隻能通過顯示類型轉換,也就是調用 time_point_cast 函數來完成該操作。

到此這篇關於C++使用chrono庫處理日期和時間的實現方法的文章就介紹到這瞭,更多相關C++ chrono日期和時間處理內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: