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;
Rep:這是一個數值類型,表示時鍾數(周期)的類型(默認為整形)。若 Rep 是浮點數,則 duration 能使用小數描述時鍾周期的數目。
Period:表示時鍾的周期,它的原型如下:
// 定義於頭文件 <ratio> template< std::intmax_t Num, std::intmax_t Denom = 1 > class ratio;
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); return 0; }
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"; return 0; }
輸出的結果為:
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; return 0; }
程序輸出的結果:
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; cout << d3.count() << endl; return 0; }
對於分子 6,、9 最大公約數為 3,對於分母 7、5 最小公倍數為 35,因此推導出的時鍾周期為 ratio<3,35>
31
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 atime_point value
算術運算符 (非成員函數) operator- tp - dtn a time_point value
算術運算符 (非成員函數) operator- tp - 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_point<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); return 0; }
今天的日期是: 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; }
print 1000 stars .... **************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************** 總共耗時: 11032200納秒
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 9.7927 ms, or 9 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 return 0; }
注意事項:關於時間點的轉換如果沒有沒有精度的損失可以直接進行隱式類型轉換,如果會損失精度只能通過顯示類型轉換,也就是調用 time_point_cast 函數來完成該操作。