在本章中,我们将开始探索Linux环境中可用的各种计时器。随后,我们将深入了解时钟的重要性,并探讨UNIX时间的概念。接下来,我们将揭示在Linux中使用POSIX准确测量时间间隔的方法。之后,我们将进入std::chrono
的领域,检查C++为有效的时间相关操作所提供的能力。我们的旅程接着将转向对std::chrono
框架中定义的持续时间、时间点和时钟的全面检查。继续前进,我们将熟悉std::chrono
内我们可以使用的各种时钟。在我们的探索路径中,我们将开始利用std::chrono
提供的日历功能。在我们探索的最后阶段,我们将熟悉时区,并提升我们使用std::chrono
的强大工具进行无缝时间转换的专业技能。
在本章中,我们将覆盖以下主要主题:
- 探索Linux中的计时器
- 处理C++中的时间
- 使用时钟、计时器和比率
- 使用日历和时区功能
那么,让我们开始吧!
技术要求
本章中的所有示例均在以下配置的环境中进行了测试:
- Linux Mint 21 肉桂版。
- GCC 13.2,编译器标志:
-std=c++20
。 - 稳定的互联网连接。
- 请确保您的环境至少是这么新的。对于所有示例,您也可以替代使用 https://godbolt.org/。
在Linux中处理时间
计时是任何计算机系统的基本方面,Linux也不例外。在Linux中,有不同类型的计时器可用,每种设计用于处理特定的任务和需求。
这些计时器可用于测量程序的执行时间、安排任务、触发事件等。在本节中,我们将探索Linux中可用的不同类型的计时器以及如何有效地使用它们。
以下是Linux系统中使用的不同类型的计时器:
- 系统计时器:Linux内核使用系统计时器来跟踪时间并安排各种任务。系统计时器用于测量系统运行时间、延迟和超时。Linux中最重要的系统计时器是 Jiffies 计时器,它在系统时钟的每个滴答中增加1。Jiffies计时器用于跟踪自系统启动以来的时间流逝,并且经常被各种内核模块和驱动程序使用。
- 实时时钟(RTC):RTC是一个硬件时钟,即使系统关闭也会保持日期和时间的跟踪。Linux内核可以通过
/dev/rtc
设备文件或hwclock
命令行工具读取和设置RTC。RTC用于在启动时同步系统时间,并为系统事件维护准确的时间戳。 - 高分辨率计时器(HRTs):HRTs提供纳秒级分辨率,适用于需要精确计时的实时应用。HRTs可用于测量代码段的执行时间、高精度安排事件或驱动高速硬件。
- POSIX计时器:POSIX计时器是POSIX标准定义的一组计时器,为Linux中的计时器管理提供统一接口。POSIX计时器可用于设置一次性或周期性计时器,这些计时器可以通过信号或线程触发。POSIX计时器是通过
timer_create()
、timer_settime()
和timer_delete()
系统调用实现的。 - 计时器队列:计时器队列是Linux内核提供的一种机制,用于安排事件和超时。计时器队列作为事件的优先级队列实现,每个事件都与一个计时器相关联。计时器队列可用于安排周期性任务、实现超时或在特定间隔触发事件。计时器队列在各种内核模块和设备驱动程序中被广泛使用。
但在谈论计时器之前,我们首先需要理解计算机系统中时间的含义。让我们看一下。
Linux纪元
在计算中,纪元指的是用作特定系统或上下文中测量时间的参考点的特定时间点。它作为其他时间值计算或表示的起点。换句话说,这是计算机从中测量系统时间的时间。
纪元通常被定义为一个特定的时间点,通常表示为自特定纪元时间以来经过的秒数、毫秒数或其他比毫秒更小的时间间隔。纪元的选择取决于系统和上下文。例如,在UNIX-like系统中(Linux就是这样的系统),纪元定义为 1970年1月1日00:00:00 UTC(协调世界时间)。这个纪元时间通常被称为 UNIX纪元 或 UNIX时间。在基于UNIX的系统中,时间值通常表示为自UNIX纪元以来经过的秒数。
现在,我们对UNIX纪元有了更好的理解,让我们看一些实际使用这些计时器的示例。
在Linux中使用计时器
既然我们已经知道Linux中可用的不同类型的计时器,让我们探索如何在我们的应用程序中使用它们。我们将看一个示例,它启动一个POSIX计时器并等待直到它被触发:
#include <iostream>
#include <csignal>
#include <unistd.h>
#include <sys/time.h>
#include <atomic>
static std::atomic_bool continue_execution{true};
int main() {
struct sigaction sa{};
sa.sa_handler = [](int signum) {
// Timer triggered, stop the loop.
std::cout << "Timer expired. Stopping the
task...\n";
continue_execution = false;
};
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGALRM, &sa, nullptr);
// Configure the timer to trigger every 1 seconds
struct itimerval timer{
.it_interval{.tv_sec{1}, .tv_usec{0}},
.it_value{.tv_sec{1}, .tv_usec{0}}
};
// Start the timer
setitimer(ITIMER_REAL, &timer, nullptr);
std::cout << "Timer started. Waiting for timer
expiration...\n";
// Keep the program running to allow the timer to
trigger
while (continue_execution) {
sleep(1);
}
return 0;
}
在本示例中,我们定义了一个当计时器到期时将被调用的lambda处理函数。在处理函数中,我们打印出一个消息,指示计时器已到期,并设置忙碌循环的退出条件。
我们使用sigaction
函数设置信号处理程序。然后,我们使用itimerval
结构的it_interval
和it_value
成员配置计时器。配置计时器后,我们通过调用带有ITIMER_REAL
选项的setitimer
POSIX函数来启动它,该选项设置一个实时计时器,在到期时发送SIGALRM
信号。我们进入一个循环以使程序无限期地运行。循环内的sleep(1)
调用确保程序不会立即退出,并允许计时器触发。
程序的输出如下:
Program returned: 0
Timer started. Waiting for timer expiration...
Timer expired. Stopping the task...
软件开发中的另一个常见任务是测量代码段的执行时间。这也可以通过使用POSIX时间功能来实现。要测量代码段的执行时间,我们可以在POSIX中使用HRT(高分辨率计时器)。
要在POSIX中使用HRT,我们将使用clock_gettime()
函数以及CLOCK_MONOTONIC
时钟ID。以下是一个演示在POSIX中使用HRTs的示例:
#include <iostream>
#include <ctime>
static const auto LIMIT{10000};
void just_busy_wait_f() {
for (auto i{0}; i < LIMIT; ++i) {
for (auto j{0}; j < LIMIT; ++j);
}
}
int main() {
timespec start, end;
// Start the timer
clock_gettime(CLOCK_MONOTONIC, &start);
// Measured code segment
just_busy_wait_f();
// Stop the timer
clock_gettime(CLOCK_MONOTONIC, &end);
// Calculate the elapsed time
const auto elapsed{(end.tv_sec - start.tv_sec) +
(end.tv_nsec - start.tv_nsec) / 1e9};
std::cout << "Elapsed time: " << elapsed << "
seconds\n";
return 0;
}
在这个示例中,我们声明了两个timespec
结构体start
和end
,以保存计时器的开始和结束时间戳。我们使用clock_gettime()
函数获取具有高分辨率的当前时间。
我们调用clock_gettime()
两次:一次在任务开始时(记录开始时间),一次在结束时(记录结束时间)。使用的是CLOCK_MONOTONIC
时钟ID,它代表一个不受系统时间调整影响的单调时钟。
捕获开始和结束时间戳后,我们通过减去时间戳的秒和纳秒组成部分来计算经过的时间。然后以秒为单位打印结果。
我们测试实验室的示例输出如下:
Program returned: 0
Elapsed time: 0.169825 seconds
请记住,在您的环境中,结果可能会有所不同。
请注意,这个示例演示了使用计时器测量执行时间的一种方法。根据您的需求,您可以选择不同的计时器机制。
POSIX计时器的特性
让我们来看一下POSIX计时器具有的一些特性:
- 功能强大且灵活:POSIX计时器提供了丰富的特性集,包括不同的计时器类型(例如间隔计时器和一次性计时器)、各种时钟来源以及对计时器行为的精确控制。
- 底层控制:POSIX计时器提供对计时器设置的细粒度控制,例如信号处理和计时器到期行为。
- 遗留支持:POSIX计时器是POSIX API的一部分,长期以来一直在UNIX-like系统中可用,如果您需要与遗留代码或特定的POSIX要求保持兼容性,则它们非常合适。
- 平台特定:POSIX计时器并非在所有平台上都可用,因此如果可移植性是您关注的问题,最好切换到更合适的选择。
但是,在C++中,我们有什么更好的替代方案呢?我们将在下一节中看到。
在C++中处理时间
虽然POSIX计时器有其自身的优点,但在C++中,有些库提供了更高级别和更可移植的解决方案,用于计时和与时间相关的操作。
一个很好的示例是std::chrono
库。这是一个C++库,为处理与时间相关的操作和测量提供了一套实用工具。它是标准库的一部分,并包含在<chrono>
头文件中。std::chrono
库提供了一种灵活和类型安全的机制,用于表示和操作时间持续时间、时间点、时钟和与时间相关的操作。通过使用std::chrono
,您将受益于C++标准库提供的标准化、类型安全性、灵活性和集成性。与传统的POSIX方法相比,std::chrono
的一些优势如下:
- 标准化:
std::chrono
是C++标准库的一部分,使其成为一个跨平台解决方案,可在不同的操作系统和编译器上一致地工作。相比之下,POSIX特定于UNIX-like系统,在所有平台上可能无法使用或行为不一致。 - 类型安全性:
std::chrono
提供了时间持续时间和时间点的类型安全表示。它提供了丰富的持续时间和时钟类型集,可以无缝使用在一起,使代码更安全和更具表现力。POSIX计时器虽然功能强大,但通常依赖于低级类型,如timespec
结构体,这可能容易出错并需要手动转换。 - 灵活性和表现力:
std::chrono
提供了一个灵活且富有表现力的时间相关操作接口。它提供了方便的方式来对持续时间执行算术运算、在不同时间单位之间转换以及格式化时间值。POSIX计时器虽然适用于特定的计时要求,但缺乏std::chrono
提供的高级抽象和实用工具。 - 与标准库的集成:
std::chrono
与C++标准库的其他部分无缝集成。它可以与算法、容器和并发设施一起使用,允许更加协调和高效的代码。POSIX计时器作为一个更低级别的接口,可能需要额外的工作才能与其他C++标准库组件集成。 - 与现代C++功能的兼容性:
std::chrono
受益于在现代C++中引入的进步和特性。它支持用户定义字面量、lambda函数和类型推导等特性,使编写简洁和富有表现力的代码变得更容易。POSIX计时器作为POSIX API的一部分,可能无法完全利用现代C++语言特性。
<chrono>
库提供了一套全面的特性集,用于处理与时间相关的操作,如测量时间持续时间、表示时间点以及执行各种时间计算和转换。以下是std::chrono
的一些关键组件和特性:
- 时钟:
<chrono>
定义了几种时钟类型,代表着不同的时间来源和不同的纪元。std::chrono::system_clock
代表了系统范围的实时时钟(RTC),可以调整。std::chrono::steady_clock
代表一个稳定的单调时钟,不受系统时间调整的影响,而std::chrono::high_resolution_clock
代表具有最高可用分辨率的时钟(如果系统支持)。 - 持续时间:
std::chrono::duration
模板类表示一个时间间隔,即指定的时间段。持续时间是使用特定时间单位的滴答计数;例如,五小时的持续时间是单位 小时 的五个滴答。可以定义不同类型的持续时间,从年到纳秒。示例持续时间包括std::chrono::seconds
、std::chrono::milliseconds
和std::chrono::months
。 - 时间点:时间点表示与特定时钟的纪元相比的特定时间点。
std::chrono::time_point
模板类由时钟和持续时间类型参数化。 - 时间转换:
std::chrono
允许在持续时间和时间点之间进行转换,以及涉及持续时间的算术运算。它提供了例如std::chrono::duration_cast
的函数,以在不同持续时间之间转换,以及std::chrono::time_point_cast
在不同时间点之间转换。 - 时钟实用工具:
std::chrono
提供了查询当前时间的实用工具,如std::chrono::system_clock::now()
,它返回当前系统时间点。 - Chrono字面量:
std::chrono
在std::literals::chrono_literals
命名空间中提供了用户定义的、与时间相关的字面量。它们允许您使用带有时间单位的字面量创建std::chrono::duration
对象。这使得在处理与时间相关的计算时代码更加易读和方便。 - 日历:
std::chrono
提供了日历功能,例如处理天、月和年。它还提供了闰年和闰秒的标记。 - 时区:
std::chrono
提供了根据地理位置不同的全球各个时区的信息。
通过使用std::chrono
,您可以进行准确且可移植的时间测量,处理超时,计算时间差异,并以类型安全的方式处理与时间相关的操作。
重要说明
以下是C++参考文档中<chrono>
头文件的链接:C++ <chrono>
文档。
以下是一个示例,展示如何使用std::chrono
来测量代码片段的执行时间:
#include <iostream>
#include <chrono>
using namespace std::chrono;
int main() {
const auto start{steady_clock::now()}; // {1}
just_busy_wait_f(); // {2}
const auto end{steady_clock::now()}; // {3}
const auto dur{duration_cast<milliseconds>(end -
start)}; // {4}
std::cout << "Execution time: " << dur.count() << "
milliseconds\n"; // {5}
return 0;
}
在上述示例中,使用std::chrono::steady_clock
来测量与前一个示例中相同函数的执行时间(见标记{2}
)。start
和end
变量代表使用steady_clock
的now()
静态函数,在代码执行前后所取的时间点(见标记{1}
和{3}
)。使用std::chrono::duration_cast
将时间点之间计算出的持续时间转换为毫秒(见标记{4}
)。
程序的输出应该类似于这样:
Program returned: 0
Execution time: 179 milliseconds
正如您所见,std::chrono::duration
类有一个count()
方法,它返回特定持续时间中的单位数;见标记{5}
。
但让我们更深入地了解这是如何真正工作的。
使用时钟、计时器和比率
在继续介绍更多有关时钟和计时器的示例之前,我们首先需要更好地理解chrono库是如何定义持续时间的。
正如我们在前面的示例中看到的,持续时间是两个时间点之间的距离,称为时间点。在我们之前的示例中,这些是start
和end
时间点。
图8.1 – 时间点和持续时间
持续时间本身是滴答数的计数和一个分数的组合,该分数表示从一个滴答到下一个滴答的时间(以秒为单位)。这个分数由std::ratio
类表示。以下是一些例子:
using namespace std::chrono;
constexpr std::chrono::duration<int, std::ratio<1>>
six_minutes_1{360};
constexpr std::chrono::duration<double, std::ratio<3600>>
six_minutes_2{0.1};
constexpr std::chrono::minutes six_minutes_3{6};
constexpr auto six_minutes_4{6min};
std::cout << six_minutes_1 << '\n';
std::cout << six_minutes_2 << '\n';
std::cout << six_minutes_3 << '\n';
std::cout << six_minutes_4 << '\n';
static_assert(six_minutes_1 == six_minutes_2);
static_assert(six_minutes_2 == six_minutes_3);
static_assert(six_minutes_3 == six_minutes_4);
在上述示例中,我们以几种方式定义了六分钟的持续时间。在six_minutes_1
变量中,我们将这个持续时间指定为360秒的值。同样的持续时间也可以表示为1小时的1/10 - six_minutes_2
变量。最后两个持续时间 - six_minutes_3
和six_minutes_4
- 使用std::chrono
预定义的持续时间类型和字面量表示相同的六分钟持续时间。以下是前面代码块的输出:
360s
0.1h
6min
6min
正如您所见,std::duration
还提供了漂亮的格式化功能,这样一旦将持续时间传递给字符串或流操作符,它将添加相应的后缀,这样我们就可以看到持续时间的类型。
为了确保前面的持续时间真的对应于六分钟,我们使用了static_assert
对它们进行了测试,如果它们不匹配,程序将失败。
重要说明
以下是C++参考文档中std::duration
类的链接:C++ std::duration
文档。
让我们回到之前的示例,稍作修改,并更仔细地查看timepoint
对象:
using namespace std::chrono;
const time_point start{steady_clock::now()}; // {1}
const duration epoch_to_start{start.time_since_epoch()}; //
{2}
std::cout << "Time since clock epoch: " << epoch_to_start
<< '\n'; // {3}
正如您所见,我们再次构造了一个timepoint
对象start
,其中获取了其实例化时刻来自Linux系统的steady_clock
的时间;见标记{1}
。std::chrono::time_point
类存储了一个std::chrono::duration
值,它实际上表示从时钟纪元开始的时间间隔。为了允许获取该值,std::chrono::duration
类公开了一个方法,返回time_since_epoch()
的持续时间,以纳秒为单位;见标记{2}
。
以下是我们测试环境中执行前述代码的结果。请记住,如果您执行此代码,结果可能会有所不同:
Time since clock epoch: 2080809926594ns
在某些用例中,持续时间以纳秒为单位可能不方便,例如我们的示例中计算代码块执行所需的时间。然而,将持续时间从更高精度的类型转换为更低精度的类型会导致精度丢失。因此,如果我们需要以分钟而不是纳秒来查看持续时间,我们不能只做这个:
using namespace std::chrono;
const minutes
dur_minutes{steady_clock::now().time_since_epoch()};
这是因为前述代码将无法编译。其背后的原因是time_since_epoch()
方法返回的持续时间精度为纳秒。如果我们将数据存储为分钟,我们肯定会失去精度。为了确保这不会被错误地完成,编译器会阻止我们。
但我们如何有意地将持续时间值从一种精度转换为另一种精度呢?正如我们在第一个示例中看到的,我们可以使用库提供的std::chrono::duration_cast
函数。它使我们能够从具有更高精度的持续时间类型转换为具有更低精度的持续时间类型。让我们重新处理前述示例,看看这是如何工作的:
using namespace std::chrono;
auto dur_from_epoch{steady_clock::now()
.time_since_epoch()}; // {1}
minutes dur_minutes{duration_cast<minutes>
(dur_from_epoch)}; // {2}
std::cout << "Duration in nanoseconds: " << dur_from_epoch
<< '\n'; //{3}
std::cout << "Duration in minutes: " << dur_minutes <<
'\n'; //{4}
正如标记{1}
中所见,我们再次从时钟纪元获取以纳秒为单位的持续时间。在标记{2}
中,我们初始化了另一个持续时间变量,这次是以分钟为单位。为此,我们使用std::chrono::duration_cast<minutes>
,它将源分辨率的值转换为目标分辨率,并将其截断为最接近的整数值。在我们的测试环境中,前述代码块的结果如下:
Duration in nanoseconds: 35206835643934ns
Duration in minutes: 586min
我们可以看到,以纳秒为单位测量的持续时间大约相当于586.78分钟,但被截断为586分钟。
当然,我们可能也需要向上取整而不仅仅是向下截断值。幸运的是,chrono
库提供了这种能力,即std::chrono::round
方法,它正好做到了这一点。这是一个示例:
using namespace std::chrono;
seconds dur_sec_1{55s}; //{1}
seconds dur_sec_2{65s}; //{2}
minutes dur_min_1{round<minutes>(dur_sec_1)}; //{3}
minutes dur_min_2{round<minutes>(dur_sec_2)}; //{4}
std::cout << "Rounding up to " << dur_min_1 << '\n';
std::cout << "Rounding down to " << dur_min_2 << '\n';
在这个示例中,我们定义了两个持续时间变量,dur_sec_1
和dur_sec_2
。dur_sec_1
初始化为55秒(见标记{1}
),dur_sec_2
初始化为65秒(见标记{2}
)。然后,使用std::chrono::round
函数,我们用分钟的分辨率初始化了另外两个持续时间变量(见标记{3}
和{4}
)。两个持续时间变量都被四舍五入到一分钟:
Rounding up to 1min
Rounding down to 1min
chrono
库还提供了用于持续时间的ceil
和floor
方法。所有这些都可以在官方文档中找到。
重要说明
round
、floor
和ceil
方法的持续时间文档可以在以下链接中找到:std::chrono::duration::round
、std::chrono::duration::floor
和std::chrono::duration::ceil
。
既然我们对时间操作有了更好的理解,让我们更仔细地看看std::chrono
为我们提供的不同类型的时钟。
C++20中关于时钟的更多信息
我们在之前的示例中已经使用了std::chrono::steady_clock
。这只是C++ chrono
库中预定义的时钟之一,您可以使用它。std::chrono::steady_clock
顾名思义,是一个稳定的时钟。这意味着它是一个单调时钟,其中时间只向前移动,其时间点值始终在增加。当我们想要测量时间间隔时,它适用于使用。它的纪元可能会有所不同。
另一个经常使用的时钟是std::chrono::system_clock
。在Linux中,它代表系统测量的时间。这意味着它不能保证是单调的,而且可以随时调整。在Linux中,它的纪元与UNIX纪元相匹配。让我们看一个例子:
using namespace std::chrono;
time_point<system_clock> systemClockEpoch;
std::cout << std::format("system_clock epoch:
{0:%F}T{0:%R%z}.", systemClockEpoch) << '\n';
前述示例打印出Linux系统时钟纪元,它对应于UNIX纪元 - 1970年1月1日00:00:00 UTC
:
system_clock epoch: 1970-01-01T00:00+0000.
请记住,std::chrono::system_clock
不考虑闰秒,这些闰秒可以从测量的时间中增加或减去。通常,闰秒是对UTC的一秒调整,这可能会每年发生两次,以反映地球绕太阳旋转的准确性。
重要说明
有关闰秒的更多信息可以在此处找到。
C++20引入了几个更多预定义的时钟。其中一些是std::chrono::utc_clock
,它测量UTC,以及std::chrono::tai_clock
,它测量国际原子 时间(TAI)。
重要说明
有关UTC和TAI的更多信息可以在此处和此处找到。
TAI时钟和UTC时钟之间的一个关键区别是,UTC时钟保证会考虑自时钟纪元以来所做的闰秒校正,但TAI时钟不会考虑这些。让我们看一个例子:
using namespace std::chrono;
tai_time tai{tai_clock::now()};
utc_time utc{utc_clock::now()};
std::cout << "International atomic time (TAI): " << tai <<
'\n';
std::cout << "Coordinated universal time (UTC): " << utc <<
'\n';
在前述示例中,我们从两个时钟 - utc
和tai
- 获取当前时间。以下是结果:
International atomic time (TAI): 2023-08-04 14:02:57.95506
Coordinated universal time (UTC): 2023-08-04 14:02:20.95506
正如您所见,尽管两个时钟都在同一时间被调用,但它们显示的时间不同。它们的差异正好是37秒。这种差异来自自1972年引入闰秒以来所做的闰秒调整。
std::chrono::utc_clock
应用了闰秒调整。通过使用chrono的UTC时钟,这些闰秒调整将自动为您完成,您不需要采取任何特殊行动。因此,chrono库提供了一种转换时钟类型的方法 - std::chrono::clock_cast
,它将std::chrono::time_point
值从一个时钟转换为另一个时钟。让我们看另一个例子:
using namespace std::chrono;
tai_time tai{tai_clock::now()};
std::cout << "International atomic time (TAI): " << tai <<
'\n';
utc_time utc{clock_cast<utc_clock>(tai)};
std::cout << "Coordinated universal time (UTC): " << utc <<
'\n';
正如您所见,由chrono的TAI时钟生成的time_point tai
对象被转换为UTC时钟的时间点。结果如下:
International atomic time (TAI): 2023-08-04 14:16:22.72521
Coordinated universal time (UTC): 2023-08-04 14:15:45.72521
正如我们所预期的,TAI时钟比UTC时钟快37秒。因此,UTC不能用于正确测量时间差异,因为可能会增加或减少闰秒。
重要说明
您可以在此处找到C++ chrono库中的所有预定义时钟。
现在,既然我们对计时和时钟有了良好的了解,让我们看看C++ chrono库为日历和时区提供了哪些功能。
使用日历和时区功能
C++20为标准库引入了对日历和时区操作的全新支持。当我们谈论日历操作时,这意味着以天、月和年为单位的操作。它们与时区概念一起,允许在不同时区之间转换时间,同时考虑到时区调整,如夏令时。
让我们定义一个日期,并借助chrono
库打印它:
using namespace std::chrono;
year theYear{2023};
month theMonth{8};
day theDay{4};
std::cout << "Year: " << theYear;
std::cout << ", Month: " << theMonth;
std::cout << ", Day: " << theDay << '\n';
如您所见,std::chrono
命名空间提供了year
、month
和day
类,使处理日期变得容易。这些类的好处在于,它们提供了严格的类型和边界检查、一些加法和减法操作符以及格式化功能。前述代码的结果如下:
Year: 2023, Month: Aug, Day: 04
如您所见,将Month
变量传递给operator<<
时会应用格式化,使月份的值打印为Aug
。此外,这些类还提供了对应用值的验证和边界检查:
using namespace std::chrono;
std::cout << "Year: " << year{2023} ;
std::cout << ", Month: " << month{13};
std::cout << ", Day: " << day{32} << '\n';
在前述示例中,我们应用了无效的月份和日期。结果如下:
Year: 2023, Month: 13 is not a valid month, Day: 32 is not a valid day
如您所见,month
和day
值经过验证,当它们传递给operator<<
时,它会打印出这些值不是有效的。
year
类代表了格里高利历的一年,它使我们能够询问该年是否是闰年:
using namespace std::chrono;
sys_time now{system_clock::now()};
year_month_day today{floor<days>(now)};
std::cout << "Today is: " << today << '\n';
year thisYear{today.year()};
std::cout << "Year " << thisYear;
if (thisYear.is_leap()) {
std::cout << " is a leap year\n";
} else {
std::cout << " is not a leap year\n";
}
在这个示例中,我们首先获取当前的系统时间 - now
- 然后将其转换为year_month_day
类型的对象。这个对象代表了一个方便的基于字段的时间点。它持有year
、month
和day
对象,并允许直接访问它们。它还支持从std::chrono::sys_days
实例化,这实际上是以天为单位的系统时钟的时间点。因此,我们传递now
时间点并创建today
对象。然后,我们获取year
对象 - thisYear
- 并使用year
类的is_leap()
方法检查这是否是闰年:
Today is: 2023-08-05
Year 2023 is not a leap year
正如预期的那样,2023年不是闰年。
chrono
库大量使用operator/
来创建日期。C++20为这个操作符的参数提供了大约40种重载。让我们看一个例子:
using namespace std::chrono;
year_month_day date1{July/5d/2023y};
year_month_day date2{1d/October/2023y};
year_month_day date3{2023y/January/27d};
std::cout << date1 << '\n';
std::cout << date2 << '\n';
std::cout << date3 << '\n';
如您所见,我们通过传递新引入的chrono字面量(对于月份、天和年份)以及operator/
来创建一个year_month_day
对象。chrono为创建天提供了方便的字面量;你只需在天数值后加上d
。对于年份也是如此,你需要加上y
就可以构造一个year
对象。对于月份,chrono库为一年中的所有月份定义了命名常量。
重要说明
以下是chrono库中月份常量列表的链接:chrono库中的月份常量。
在实例化year_month_day
对象时,我们使用operator/
传递日期值。如前述示例所示,chrono支持多种组合的日、月和年值。所有这些都可以在标准文档中找到。
重要说明
以下是关于operator/
的所有重载的文档,用于日期管理:日期管理的operator/
重载。
我们示例中使用的所有重载都应该创建有效的year_month_date
对象。让我们看看输出:
2023-07-05
2023-10-01
2023-01-27
如我们所见,我们已经成功地借助chrono字面量和operator/
创建了三个不同的有效日期。
在C++中处理时区
C++20的chrono
库为处理时区提供了功能。它集成了IANA时区数据库,该数据库包含了全球许多地理位置的本地时间信息。
重要说明
有关IANA时区数据库的更多信息,请访问:IANA时区数据库。
使用chrono
,您可以获取IANA数据库的副本,并浏览特定地理位置的信息:
using namespace std::chrono;
const tzdb& tzdb{get_tzdb()};
const std::vector<time_zone>& tzs{tzdb.zones};
for (const time_zone& tz : tzs) {
std::cout << tz.name() << '\n';
}
如示例所示,在std::chrono
命名空间中,有一个方法 - get_tzdb()
- 它返回对IANA数据库的引用。在数据库中,您可以找到有关其版本的信息,还可以获取所有可用std::chrono::time_zone
对象的排序列表。
std::chrono::time_zone
类存储了其特定地理区域和名称的时区之间转换的信息。前述示例的输出如下:
Africa/Abidjan
Africa/Accra
Africa/Addis_Ababa
Africa/Algiers
Africa/Asmara
Africa/Bamako
...
现在,既然我们有了所有可用的时区,让我们尝试基于地理位置找到特定的一个,并看看那里的时间是什么样的:
using namespace std::chrono;
const tzdb& tzdb{get_tzdb()};
const std::vector<time_zone>& tzs{tzdb.zones};
const auto& res{std::find_if(tzs.begin(), tzs.end(), []
(const time_zone& tz){
std::string name{tz.name()};
return name.ends_with("Sofia");
})};
if (res != tzs.end()) {
try {
const std::string_view myLocation{res->name()};
const std::string_view london{"Europe/London"};
const time_point now{system_clock::now()};
const zoned_time zt_1{myLocation, now};
const zoned_time zt_2{london, now};
std::cout << myLocation << ": " << zt_1 << '\n';
std::cout << london << ": " << zt_2 << '\n';
} catch (const std::runtime_error& e) {
std::cout << e.what() << '\n';
}
}
在这个示例中,我们再次获取了可用时区的列表,并尝试找到索菲亚(Sofia
)城市的时区。然后,我们使用找到的时区的全名来创建另一个对象,该对象使用特定的地理位置和系统时间的值 - std::chrono::zoned_time
。这个类代表了时区和时间点之间的逻辑配对。我们还为伦敦(London
)城市创建了另一个zoned_time zt_2
对象,它代表与zt_1
相同的时间点,但在另一个地理位置。前述代码的结果如下:
Europe/Sofia: 2023-08-05 13:43:53.503184619 EEST
Europe/London: 2023-08-05 11:43:53.503184619 BST
如您所见,两个对象都显示了有效的时间,但考虑到了它们的地理位置。这就是我们如何安全地获取特定地理位置的当前时间,同时也考虑到夏令时。
总结
在本章中,我们探索了Linux环境中可用的不同计时器。随后,我们了解了时钟纪元背后的重要性和UNIX时间的概念。接着,我们深入探讨了在Linux中准确测量时间的POSIX的实际实现。此外,我们研究了std::chrono
的领域,并检查了C++为有效的时间相关操作所提供的一系列功能。我们的探索随后带我们详细了解了std::chrono
框架内定义的持续时间、时间点和时钟。接下来,我们熟悉了std::chrono
内可用的各种时钟类型。随着我们的旅程继续,我们开始探索std::chrono
所提供的日历功能。最后,我们熟悉了时区,并提高了我们利用std::chrono
所提供的工具无缝执行时间转换的熟练度。现在,我们已经准备好进入下一章,我们将更深入地探讨C++内存模型的细节。