前面提到,C++ 11 的时间库提供了各种时钟、时间点以及时间间隔的计算与表达,但是却没有提供日期相关的类型,也没有提供与时区有关的本地时间转换等支持组件,所以用起来不是很顺手。直到 C++ 20 终于补齐了这块短板,时间库具备是时间、日期和时区的完整支持,终于可以替代不安全的 C 函数接口。本篇主要就是介绍 C++ 20 新增的日历和时区相关内容。
1 Time of day
设想一个与时间显式有关的场景,我们拿到了一个当前系统时间的时间点,希望在 UI 界面上显式这个时间。C++ 11 的方法就是先用时间点的 to_time_t() 方法将其转成一个 time_t 类型的秒的计数,然后再用 C 标准库提供的 localtime() 或 gmtime() 系列函数转成离散的时间数据结构 tm,这样就得到以本地时间表示的具体的时、分、秒以及日期信息,可用于界面的显示。这个过程存在两个问题,首先是与 C 的接口互转显得很突兀,其次是将高精度的时间点转成以秒为单位的 time_t 类型计数器会丢失时间精度。当然,我们也可以硬刚 chrono 库,借助时间点和时间间隔的运算符重载拆出具体的时间信息,比如:
auto sys_now_dura = system_clock::now().time_since_epoch();
auto rest_of_day = sys_now_dura % 24h;
auto hour = rest_of_day / 60min;
auto rest_of_hours = rest_of_day % 60min;
auto minute = rest_of_hours / 60s;
auto rest_of_minutes = rest_of_hours % 60s;
auto second = duration_cast<seconds>(rest_of_minutes).count();
//输出 hour:minute:second
C++ 20 提供的hh_mm_ss
能更简单地将一个时间间隔分解成时、分、秒的形式,形如其名,定义如下:
template< class Duration >
class hh_mm_ss;
hh_mm_ss
提供了几个成员函数,可以很方便地获取分离出来的时、分、秒信息。尽管它有一个 duration 类型的模板参数,但是在使用的时候,编译器可以根据构造函数的参数类型推导出这个模板参数,所以可以省略,比如:
hh_mm_ss hms(5h+43min+12s); //自动推导的 duration 类型是 seconds
assert(hms.hours().count() == 5);
assert(hms.minutes().count() == 43);
assert(hms.seconds().count() == 12);
利用hh_mm_ss
的这个特点,获得当前时间的时、分、秒的代码就非常简单了:
hh_mm_ss hms(system_clock::now().time_since_epoch() % 24h);
auto hour = hms.hours().count(); //整数类型的小时数
auto minute = hms.minutes().count(); //整数类型的分钟数
auto second = hms.seconds().count(); //整数类型的秒数
除了hh_mm_ss
类,C++ 20 还提供了判断时间是上午时间还是下午时间的函数,以及转换 12 小时制和 24 小时制的函数。使用也是非常简单:
hh_mm_ss hms(11h+43min+12s);
assert(is_am(hms.hours()));
assert(is_pm(hours(16)));
auto half_h = make12(hours(16));
assert(half_h.hours().count() == 4);
auto full_h = make24(hours(6), true);
assert(full_h.count() == 18);
hh_mm_ss
类的构造函数和主要的成员函数都是 constexpr 类型,这意味着可以声明 constexpr 类型的hh_mm_ss
对象,同时也可以在其他常量表达式或常量函数中使用hh_mm_ss
。
2 日历
2.1 基本日历单位
2.1.1 year、month 和 day
既然时间有hh_mm_ss
,那么日期应该有year_month_day
吧?没错,不仅有year_month_day
,还有year_month
、month_day
、year_month_weekday
等等。day、month 和 year 提供了单独的年、月、日基本日历单位的表达,这些对象本身并没有复杂的功能,只是提供了基本的构造、加减运算和比较运算。比如 day,表示一个月内的日期,正常情况下,其值的范围应该是 [1-31] 区间,但是可以用 0 -255 范围内的值初始化一个 day 对象,它不抱怨并不表示它是个有效的日期,可以用成员函数 ok() 来判断一个 day 对象是否是一个有效的日期:
day md{
15 };
assert(md.ok());
md -= 4d; //前四天是几号?
assert(md == 11d);
md += 21d; //加上 21 天,变成 32
assert(!md.ok()); // 不是合法的日子
注意 std::chrono::day
和 std::chrono::days