C++11 中提供了日期和时间相关的库<chrono>,通过<chrono>库可以很方便地处理日期和时间,为程序的开发提供了便利。<chrono> 库主要包含三种类型的类:

  • 时间间隔duration
  • 时钟clocks
  • 时间点time point

时间间隔 duration

duration表示一段时间间隔,用来记录时间长度,可以表示几秒、几分钟、几个小时的时间间隔。

duration 类的原型

// 定义于头文件 <chrono>
template<
    class Rep,
    class Period = std::ratio<1>
> class duration;
  • Rep:这是一个数值类型,表示时钟数(周期)的类型(默认为整型)。

  • 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>>

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;

duration 类的使用

#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);
}

时间点 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 );

时钟 clocks

chrono 库中提供了获取当前的系统时间的时钟类,包含的时钟一共有三种:

  • system_clock:系统的时钟,系统的时钟可以修改,甚至可以网络对时,因此使用系统时间计算时间差可能不准。
  • steady_clock:是固定的时钟,相当于秒表。开始计时后,时间只会增长并且不能修改,适合用于记录程序耗时
  • high_resolution_clock:和时钟类 steady_clock 是等价的(是它的别名)。

在这些时钟类的内部有 time_pointdurationRepPeriod 等信息,基于这些信息来获取当前时间,以及实现 time_ttime_point 之间的相互转换。

在使用 chrono 提供的时钟类的时候,不需要创建类对象,直接调用类的静态方法就可以得到想要的时间了。


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 类一共提供了三个静态成员函数:

// 返回表示当前时间的时间点。
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);
}


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));
    }
};

提供了一个静态的 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;
}


high_resolution_clock
high_resolution_clock 提供的时钟精度比 system_clock 要高,它也是不可以修改的。在底层源码中,这个类其实是 steady_clock 类的别名。

using high_resolution_clock = steady_clock;

转换函数

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";
}


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
}


项目中常用方法

实现鼠标双击判断

// 用于存储上次点击的时间和位置
struct ClickInfo {
	std::chrono::steady_clock::time_point time;
	double x, y;
};

ClickInfo lastClick = { std::chrono::steady_clock::now(), 0.0, 0.0 };
const double DOUBLE_CLICK_TIME_THRESHOLD = 0.3; // 双击时间阈值(秒)
const double DOUBLE_CLICK_DISTANCE_THRESHOLD = 5.0; // 双击距离阈值(像素)

// 鼠标点击回调
void mouse_click_callback(GLFWwindow* window, int button, int action, int mods)
{
	if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_PRESS) {
		double x, y;
		glfwGetCursorPos(window, &x, &y);

		auto now = std::chrono::steady_clock::now();
		auto duration = std::chrono::duration_cast<std::chrono::duration<double>>(now - lastClick.time).count();

		// 检查时间和距离阈值
		if (duration <= DOUBLE_CLICK_TIME_THRESHOLD &&
			std::abs(x - lastClick.x) <= DOUBLE_CLICK_DISTANCE_THRESHOLD &&
			std::abs(y - lastClick.y) <= DOUBLE_CLICK_DISTANCE_THRESHOLD) {
			// 触发双击回调
			CallbackDlbClicked();
		}

		// 更新上次点击的信息
		lastClick.time = now;
		lastClick.x = x;
		lastClick.y = y;
	}
}

实现循环中延时控制循环最大刷新率

// 设置目标帧率为 24 FPS
const double targetFrameTime = 1.0 / 24.0; // 每帧的目标时间(秒)

// render loop
while (!glfwWindowShouldClose(mpWindow)) 
{
	auto startTime = std::chrono::high_resolution_clock::now();

	//----------
	//Do Something...

	// 计算帧耗时
	auto endTime = std::chrono::high_resolution_clock::now();
	std::chrono::duration<double> frameTime = endTime - startTime;

	// 计算需要的延迟时间
	double sleepTime = targetFrameTime - frameTime.count();
	if (sleepTime > 0) {
		// 睡眠以达到目标帧率
		std::this_thread::sleep_for(std::chrono::duration<double>(sleepTime));
	}

	// 输出实际帧率(可选)
	CString str;
	str.Format("Frame Time: %fs, Sleep Time: %fs\n", frameTime.count(), sleepTime);
	OutputDebugString(str);
}

计算算法耗时

// 开始计时
auto start = std::chrono::high_resolution_clock::now();

//Do Something...

// 结束计时
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> duration_ms = end - start;

// 算法耗时,毫秒
mElapsedTime = duration_ms.count();
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐