CppCon 2015 学习:A C++14 Approach to Dates and Times
Big Picture — 日期库简介
- 扩展 标准库
这个库是对 C++ 标准库中<chrono>
的自然延伸,专注于处理“日历”相关的功能(比如年月日、闰年、节假日等),而不仅仅是时间点和时长。 - 极简设计
它是**单头文件(header-only)**库,轻量且方便集成。 - 功能有限,但基础扎实
这个库不会帮你完成所有日期相关的复杂需求,但提供了高效且可靠的基础模块,你可以基于它自由构建自己的日期和日历功能。
换句话说,这个库不是“全功能的日期解决方案”,而是给程序员提供了“积木块”,用来灵活地实现各种自定义的日期处理。
这部分是描述日期时间库(如 date.h
和 tz.h
)在整个时间处理系统中的位置。以下是详细理解:
图示结构(从底层到上层)
hardware↑
OS↑
<chrono> ← C++ 标准时间库(时长、时间点等)↑
"date.h" ← 扩展 <chrono> 的日期处理(如年/月/日)
"tz.h" ← 时区支持,使用 IANA tz 数据库↑
IANA tz database ← 提供全世界时区规则数据
NTP Server ← 用于同步当前精确时间
各部分说明
部件 | 作用 |
---|---|
hardware | 提供系统时钟的硬件层 |
OS | 操作系统控制时间访问和管理 |
<chrono> | C++ 标准时间库,处理时间点/持续时间 |
date.h | 更高层的日期处理(年/月/日等),对 <chrono> 的扩展 |
tz.h | 基于 IANA 时区数据库的时区支持 |
IANA tz database | 世界标准的时区定义数据库(例如夏令时规则) |
NTP Server | 网络时间协议,用于获取当前标准时间 |
本讲内容集中在哪里?
- 重点在
date.h
和tz.h
:
如何在 C++ 中用现代、高效、跨平台的方式处理日期和时区问题
这为跨时区处理、日历计算、时间比较等功能提供了基础。
这一部分说明了这个日期库(如 date.h
)在用户代码(client code)中的位置与使用方式。它表达的是这个库的架构理念和接口层次。理解如下:
分层结构
Client Code↑
Cute API ← 简洁易用的高级接口(例如:"2025-06-03")↑
Type-Safe Objects ← 严格类型封装的日期/时间对象↑
Date Algorithms ← 内部封装的日期计算逻辑
各层职责说明
层级 | 描述 |
---|---|
Client Code | 应用层开发者写的业务代码 |
Cute API | 高层语法糖接口,简洁好读(如 "2025y/6/3" ) |
Type-Safe Objects | 比如 year , month , day , year_month_day 等强类型对象,避免误用 |
Date Algorithms | 底层封装的算法,比如判断闰年、月天数、日期加减等逻辑 |
核心理念
- 你可以选择用:
- 简洁、表达力强的 “Cute API” 来提高代码可读性
- 或直接操作 类型安全对象(type-safe objects) 获得精细控制
- 所有的算法实现都隐藏在类型安全对象里,比如
year_month_day
内部处理所有的闰年/月份/天数验证等,不需要你操心
举例说明
#include "date/date.h"
using namespace date;
using namespace std::chrono;
year_month_day ymd = 2025_y/6/3; // Cute API
auto sys_days = sys_days(ymd); // 转为系统时间
auto ymd2 = year_month_day{sys_days}; // 类型安全地再转回来
你既可以用 "2025_y/6/3"
这样的简洁表达,也可以用 year
, month
, day
构造更底层对象,这两种方式之间是可互换的。
这段是对日期库中一个核心日期算法的解析:
constexpr int days_from_civil(int y, unsigned m, unsigned d) noexcept {y -= m <= 2;const Int era = (y >= 0 ? y : y - 399) / 400;const unsigned yoe = static_cast<unsigned>(y - era * 400);const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;return era * 146097 + static_cast<Int>(doe) - 719468;
}
将 {year, month, day}
转换成“距某一固定日期的天数”(序列化天数)。这个过程叫做**“civil date → days”** 转换。
理解核心函数:days_from_civil(...)
函数作用:
将公历日期 {年, 月, 日}
转换为一个整数(表示相对某个“起点”的总天数)。
这个“起点”通常是:1970-01-01(UNIX epoch) 或 0000-03-01 或其他定义。
参数说明:
constexpr int days_from_civil(int y, unsigned m, unsigned d)
y
:年份m
:月份(1-12)d
:日(1-31)
主要计算过程(逐行解释):
y -= m <= 2;
- 如果是1月或2月,就把年份减1(因为算法以3月为起始月,有助于统一闰年处理)
const int era = (y >= 0 ? y : y - 399) / 400;
- 把年份分成以400年为周期的“纪元”,以处理格里历周期性(400年是格里历的一个周期)
const unsigned yoe = static_cast<unsigned>(y - era * 400);
yoe
: year of era,即当前纪元内的年数(范围:0~399)
const unsigned doy = (153*(m + (m > 2 ? -3 : 9)) + 2)/5 + d - 1;
- 计算年内的天数 day-of-year (doy),基于3月为起点的公式
- 这样处理2月(闰年)的问题更简单、统一
const unsigned doe = yoe * 365 + yoe/4 - yoe/100 + doy;
doe
: day-of-era,计算当前纪元开始以来的总天数- 包括平年天数
365 × yoe
- 加上闰年天数
+ yoe/4 - yoe/100
(每4年1闰,百年不闰)
- 包括平年天数
return era * 146097 + static_cast<Int>(doe) - 719468;
- 结合“纪元”和“纪元内天数”计算出总天数
146097
是 400 年内总天数:365×400 + 97(闰年)
719468
是一个偏移量,用于将基准点转为 Unix epoch 或其他系统的起始点
应用场景
- 快速比较两个日期之间的天数差
- 实现
operator<
或operator-
之类的日期操作 - 是
std::chrono::sys_days
等类型背后的基础
你现在看到的是上一函数的“反操作”版本,也就是将序列化天数(自某固定日期起算)转换为 {year, month, day}
三元组的算法。这个函数名为:
constexpr std::tuple<int, unsigned, unsigned> civil_from_days(int z) noexcept {z += 719468;const Int era = (z >= 0 ? z : z - 146096) / 146097;const unsigned doe = static_cast<unsigned>(z - era * 146097);const unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;const Int y = static_cast<Int>(yoe) + era * 400;const unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100);const unsigned mp = (5 * doy + 2) / 153;const unsigned d = doy - (153 * mp + 2) / 5 + 1;const unsigned m = mp + (mp < 10 ? 3 : -9);return std::tuple<Int, unsigned, unsigned>(y + (m <= 2), m, d);
}
civil_from_days(int z)
作用:
给定一个整数
z
,表示自公历参考点(如 1970-01-01 或 0000-03-01)以来的天数,返回其对应的{年, 月, 日}
。
这是 days_from_civil
的逆函数,两个函数可以互相还原。
参数:
z
:序列化天数(整数)。例如,0
可能表示1970-01-01
(取决于基准)。
逐行解释:
z += 719468;
- 把天数偏移到公历系统的某个统一参考点(通常对应于
0000-03-01
)
const Int era = (z >= 0 ? z : z - 146096) / 146097;
- 计算是哪一个 400 年纪元(每纪元是 146097 天 = 365×400 + 97 闰年天)
const unsigned doe = static_cast<unsigned>(z - era * 146097);
- 纪元内的天数
doe = day-of-era
const unsigned yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365;
- 计算纪元内的年份
yoe = year-of-era
(范围 0~399),包含闰年修正
这是“逆推”的关键计算:从天数恢复年份
const Int y = static_cast<Int>(yoe) + era * 400;
- 总年份
y
是当前纪元的年份加上纪元前的年份
const unsigned doy = doe - (365*yoe + yoe/4 - yoe/100);
- 计算该年内的第几天(day-of-year)
const unsigned mp = (5*doy + 2)/153;
- 将 day-of-year 映射为月段索引
mp
,这里仍以3月为起点(使2月变成年末)
const unsigned d = doy - (153*mp+2)/5 + 1;
- 从
mp
反推具体的“日”
const unsigned m = mp + (mp < 10 ? 3 : -9);
- 从
mp
反推具体的“月”(注意它是基于3月起算的)
return std::tuple<Int, unsigned, unsigned>(y + (m <= 2), m, d);
- 如果是1月或2月,年份要补回刚开始减掉的1年
- 返回
{year, month, day}
与 days_from_civil
配套使用:
int days = days_from_civil(2025, 6, 3);
auto [y, m, d] = civil_from_days(days); // 还原回 2025, 6, 3
应用场景:
- 构造日历
- 日期加减、间隔计算
std::chrono::sys_days → date
显示格式转换
日期算法的鲁棒性和覆盖范围非常强:
- 所有提供的日期算法(如
days_from_civil()
和civil_from_days()
等)
都经过了严格的单元测试。 - 测试范围覆盖了从现在往前和往后各一百万年,即总共约 200 万年的日期。
为什么这很重要?
- 日历系统的规则(如闰年、纪元、历法转换)是复杂的;
- 即使在极端日期范围(比如公元前999,999年),算法也仍然工作正确;
- 表明这些算法非常可靠、健壮,甚至远超实际工程需求(实际我们只需几百年范围)。
实际意义:
- 你可以放心地使用这些算法,不必担心日期出错;
- 它们适用于历史研究、天文计算、未来预测等;
- 对于需要精度和长期一致性的系统(如日历服务、时间戳系统等),特别有价值。
这部分讲的是 “类型安全的日期对象(Type-Safe Objects)”,是现代 C++ 日期库(如 date.h
)中提供的一种 安全、清晰、结构化表示日期的方式。
核心概念:类型安全的日期对象
这些类 不是简单的整数或字符串,而是具备语义的结构体,让日期处理更直观、安全、易于组合。
1. year_month_day
- 典型的日期结构,表示一个明确的年月日。
- 示例:
year_month_day ymd{2025y, June, 3d}; // 2025-06-03
2. year_month_day_last
- 表示某月的最后一天(无需手动判断是28/30/31日)。
- 示例:
year_month_day_last ymdl{2025y, month_day_last{June}};
3. year_month_weekday
- 表示类似“2025年6月的第1个星期一”。
- 示例:
year_month_weekday{2025y, June, weekday_indexed{Monday, 1}};
4. year_month_weekday_last
- 表示类似“2025年6月的最后一个星期五”。
- 示例:
year_month_weekday_last{2025y, June, weekday_last{Friday}};
5. weekday
- 表示一周中的哪一天(Monday、Tuesday 等)。
- 示例:
weekday wd{2025, 6, 3}; // 会计算出星期几
6. day_point
(或 sys_days
)
- 时间轴上的绝对日期点(通常是自1970年1月1日起的某天数)。
- 它是所有转换和运算的核心 —— 可以看作“中枢神经”。
- 示例:
sys_days dp = 2025y/6/3; // day_point,类似 time_point
总结:为什么这很重要?
- 比
int
或string
表示日期更安全(不易混淆年月日顺序) - 支持丰富的日期计算与转换
- 与
chrono
完美集成,可参与时间运算 - 代码更易读、维护性强
选对数据结构,比写对算法更重要
引用 Stepanov(STL 设计者)的一句话:
“选择不合适的数据结构,几乎是导致性能问题的最常见原因。”
在这个日期库中的应用:
这个日期库提供了 类型安全的数据结构(Type-Safe Objects),用于更清晰、更高效地处理日期和时间。
year_month_day
- 表示一个具体的日历日期。
- 数据结构如下:
struct year_month_day {year y;month m;day d;
};
- 示例用法:
year_month_day ymd{2025y, June, 3d}; // 表示 2025-06-03
day_point
(也叫 sys_days
)
- 表示自某个固定日期(通常是1970-01-01)以来的天数。
- 是日期计算的“底层格式”,适合数学运算。
- 数据结构如下:
struct day_point {days count; // days 是 chrono 中的类型,表示“经过了多少天”
};
- 示例:
sys_days dp = 2025y/6/3; // 自动转换为 day_point(类似时间戳)
为什么要这样设计?
year_month_day
:语义清晰,易于展示、读写day_point
:高效、适合计算(如相减得出间隔天数)- 二者可以互相转换 → 灵活组合,性能优良
鼓励你自己构建数据结构
这个库提供了基础组件,比如 year
、month
、day
,你也可以根据需要组合:
- 构建
academic_calendar_date
- 支持农历、节假日标记等
- 构建用于金融系统的“交易日”结构
这部分讲的是 类型安全对象(Type-Safe Objects) 与日期算法(Date Algorithms)之间的关系,特别强调了:
类型转换 = 自动执行日期算法
内容总结:
“Type conversions execute date algorithms.”
—— 意思是说,在类型之间转换时,系统自动运行相应的日期算法,你不需要手动调用。
示例对象:year_month_day
和 day_point
year_month_day ymd{2025y, June, 3d}; // 日历格式
day_point dp = ymd; // 自动转换为 day_point(执行算法)
year_month_day back = dp; // 再转回来(再次执行算法)
背后发生了什么?
ymd -> day_point
:调用days_from_civil(...)
day_point -> ymd
:调用civil_from_days(...)
所以关键点是:
- 这些转换 看似简单,但背后是精准的算法(已经过百万年范围的单元测试验证)
- C++ 的类型系统和重载机制 隐藏了算法的复杂性,给你一个“Cute API” —— 既好用又强大
Cute API 的好处
- 不需要用户理解底层算法
- 只需操作对象,就完成了日期推导与转换
- 代码表达力强,错误率低
小结
对象类型 | 说明 |
---|---|
year_month_day | 人类友好格式,适合输入输出 |
day_point | 机器友好格式,适合运算存储 |
类型转换 | 自动执行算法,结果可靠精确 |
这部分内容是在介绍 “Cute API” 如何结合 类型安全对象(Type-Safe Objects) 与 重载运算符 实现简洁、直观的日期构造方式。
理解内容核心:
用
/
运算符拼接字段(如 year、month、day)来构建完整日期对象。
这是一种 操作符重载(operator overloading) 技巧,让代码看起来更像人类写的日期,而不是传统结构体初始化。
具体结构
year / month / day --> year_month_day 类型
例如:
auto date = 2025y / June / 3d; // 创建一个 year_month_day 对象
2025y
是year
类型的对象June
是枚举类型month
3d
是day
类型的对象
上述表达式会被解释为:
year_month_day{year{2025}, month{6}, day{3}};
实现机制:
背后是对 /
运算符的重载,例如:
constexpr year_month operator/(const year& y, const month& m);
constexpr year_month_day operator/(const year_month& ym, const day& d);
所以 /
是组合构建器,逐步合并:
year / month
→year_month
year_month / day
→year_month_day
为什么称它为 “Cute API”?
- 阅读性强:像写自然语言
- 函数式风格:无副作用,组合清晰
- 类型安全:编译器能检测非法组合(比如
year / day
不合法)
示例完整代码:
#include <date/date.h>
#include <iostream>
using namespace date;
int main() {auto d = 2025y / June / 3d;std::cout << d << '\n'; // 输出: 2025-06-03
}
总结:
概念 | 说明 |
---|---|
Cute API | 用 / 运算符拼接字段创建日期对象 |
类型安全对象 | year, month, day, year_month_day 等 |
好处 | 清晰、直观、强类型、安全、易读 |
背后机制 | operator/ 重载实现类型组合和构建 |
这部分讲的是 Cute API 如何与 类型安全对象(Type-Safe Objects) 和 日期常量(如星期、月份、日) 一起工作,构建更丰富的日期表示,特别是像“某年3月的最后一个星期天”这样的复杂日期。
核心理解:
使用 运算符重载(
/
) 和 字面量(如sun
,last
,mar
,2015y
),构造一个表示特殊日期结构的对象:
sun[last] / mar / 2015
→ year_month_weekday_last
关键概念解释:
1. 类型 year_month_weekday_last
用于表示 “某年某月的最后一个星期几”,比如:
- 2015年3月的最后一个星期天
对应结构:
year_month_weekday_last{year{2015},month{3},weekday_last{Sunday}
}
2. 表达式解释:
sun[last] / mar / 2015y
等价于:
year_month_weekday_last{year{2015},month{3},weekday_last{weekday{0}} // Sunday is typically 0
}
3. 运算符重载过程:
表达式由一连串 /
运算拼接组成:
步骤 | 表达式 | 结果类型 |
---|---|---|
1 | sun[last] | weekday_last |
2 | weekday_last / mar | month_weekday_last |
3 | month_weekday_last / 2015y | year_month_weekday_last |
这些操作都依赖于重载的
/
运算符,像链式拼接一样。
示例代码:
#include <date/date.h>
#include <iostream>
using namespace date;
int main() {auto special_date = sun[last] / March / 2015y;std::cout << special_date << '\n'; // 输出: 2015-Mar-last-Sun
}
应用场景:
- 日历系统中找“每月最后一个星期五”
- 排程与提醒(如 DST 调整日、支付日)
- 法律/金融文档中日期计算
总结:
项目 | 描述 |
---|---|
Cute API | 利用 / 运算符重载组合字段 |
year_month_weekday_last 类型 | 表示“某年某月的最后一个星期几” |
字面量:sun , last , mar , 2015y | 用于构造类型安全、编译时常量的表达式 |
表达式:sun[last]/mar/2015y | 转换为 year_month_weekday_last |
这段内容对比了用 Cute API 和传统构造函数来创建日期对象的两种方式,重点说明了 Cute API 通过运算符重载使代码更简洁、易读,同时传统构造函数仍然可用。
核心理解:
1. Cute API(运算符重载组合字段)
auto d = sun[last] / mar / 2015;
- 通过重载的
/
运算符,将各个部分(星期、月份、年份)组合成一个year_month_weekday_last
类型。 - 看起来像自然语言,更直观。
- 这里的
sun[last]
是“最后一个星期天”的表达。
2. 传统构造函数语法
year_month_weekday_last(year(2015),month(3),weekday_last(weekday(0)) // Sunday 是 weekday 0
);
- 直接用构造函数传入具体的类型和值。
- 更显式但写起来更啰嗦。
- 可用于不支持运算符重载的环境,或者需要更清晰的类型控制时。
3. 作用对比
方式 | 优点 | 备注 |
---|---|---|
Cute API | 代码简洁,语义清晰,易读 | 依赖运算符重载和字面量 |
传统构造函数 | 明确、标准,兼容性好 | 代码较繁琐 |
4. 总结
主要点 |
---|
Cute API 是对传统构造函数的“语法糖”,让代码更漂亮 |
两者最终生成的是相同的 year_month_weekday_last 类型实例 |
开发者可以根据偏好和场景选择使用哪种方式 |
这部分内容讲的是 year_month_day
这个类型,它是日期库里的一个字段类型(field type),用来表示具体的日期(年、月、日)。重点是展示了:
核心理解:
1. 构造方式(Cute API)
auto ymd = 2015_y / sep / 25;
- 利用运算符
/
重载,把年(2015_y
)、月(sep
)、日(25
)组合成一个year_month_day
类型实例。 - 这种写法很自然,就像写日期一样。
2. 访问字段
assert(ymd.year() == 2015_y);
assert(ymd.month() == sep);
assert(ymd.day() == 25_d);
- 可以通过
year()
、month()
、day()
方法分别访问对应的日期字段。 - 这里的
2015_y
、sep
、25_d
是类型安全的字面量,保证类型和单位正确。
具体说明
元素 | 说明 |
---|---|
year_month_day | 表示年-月-日的类型安全日期结构 |
2015_y | C++ 用户定义字面量,表示年份2015 |
sep | 表示九月(September),预定义的月份常量 |
25 | 整数日数 |
25_d | 25天,带有类型的“日”字面量 |
总结
year_month_day
是日期的核心类型,代表具体的年月日。- Cute API 用
/
运算符拼接字面量生成实例,代码直观易读。 - 可以通过
.year()
、.month()
、.day()
方法访问各个部分。
这段话强调了 year_month_day
类型的几个重要特性:
理解要点
1. 构造
constexpr auto ymd = 2015_y / sep / 25;
- 使用了
constexpr
,表示这个year_month_day
对象在编译时就能被计算和确定。 - 用 Cute API 语法构造日期,语义清晰。
2. 访问字段(编译期检查)
static_assert(ymd.year() == 2015_y, "");
static_assert(ymd.month() == sep, "");
static_assert(ymd.day() == 25_d, "");
static_assert
是编译时断言,保证ymd
中的年、月、日字段值在编译时就正确。- 说明所有字段访问都支持
constexpr
,即可以在编译期完成运算和验证。
3. 总结
year_month_day
支持完全的编译期操作,提高效率且能在编译阶段捕获错误。- 这种设计对需要高性能和安全性的程序特别有用。
- 既能在编译期用作常量,也能在运行时使用,非常灵活。
这组内容详细介绍了 year_month_day
类型的用法和灵活性,重点讲了构造、算术运算、输入输出以及类型安全等。下面是详细的理解总结:
1. 构造 & 读写操作
constexpr auto ymd = 2015_y / sep / 25;
- 利用重载的
/
运算符,可以轻松构造日期。 - 支持
constexpr
,编译时就能确定日期值。
cout << ymd << '\n'; // 输出 2015-09-25
- 支持直接打印,格式化为常见的年月日形式。
2. 年月算术运算
auto next_month = ymd + months{1};
auto last_year = ymd - years{1};
- 支持用
months{}
、years{}
进行日期加减操作。 - 这种算术是基于年月日的“字段”加减。
- 如果要对天数进行操作,应该使用
day_point
类型,专门处理天数的加减。
3. 字段的灵活输入输出(I/O)
- 每个字段(年、月、日)都能与整型互相转换,方便自定义输入输出。
- 例如:
int y, m, d;
cin >> y >> m >> d;
auto ymd = year(y) / month(m) / day(d);
- 只有第一个字段必须显式转换,后续字段可直接用整型值(比如
m
、d
):
auto ymd = year(y) / m / d;
4. 字段顺序灵活
- 可以用多种顺序构造日期:
auto ymd1 = day(d) / m / y;
auto ymd2 = month(m) / d / y;
- 只要第一个字段是明确的类型(
year
、month
或day
),其余字段为整数,解析就无歧义。
5. 类型安全编译期检查
auto ymd = y / month(m) / d; // 编译错误!
- 不能混淆字段类型,比如整型
y
不能直接与month
类型做/
运算。 - 这种错误会在编译阶段被捕获,保证类型安全。
总结
year_month_day
提供了灵活、类型安全的日期表示和操作。- 方便用多种方式输入日期,同时编译器能帮你防止字段顺序或类型的错误。
- 支持方便的日期加减运算,支持打印输出。
- 设计上既保证了易用性,也保证了安全性和正确性。
这个部分讲的是日期加减时遇到“无效日期”的处理问题,以及这套库是如何设计的。总结如下:
1. 无效日期问题示例
- 比如计算:
auto ymd = 2015_y / jan / 31;
ymd += months{1}; // 2015-02-31 是无效日期
- 这是个无效日期,因为2015年2月没有31号。
- 有多种常见处理方案:
- 自动调整到有效日期(如2015-02-28)
- 抛出异常
- 返回下一个有效日期(如2015-03-03)
- 不同库、程序有不同的选择。
2. 本库的设计理念
- 库本身不做“自动修正”或“自动抛异常”,它允许你创建无效日期。
- 你必须自己选择何时检测和如何处理无效日期。
- 这带来更高性能,因为库不做额外检查,给你自由和灵活性。
- 你可以选择:
- 使用
assert()
检测有效性。 - 使用
ok()
函数检测日期是否有效。 - 自己抛异常或者做其他逻辑处理。
- 使用
3. 具体示例
断言检测:
ymd += months{1}; // ymd 变成了 2015-02-31(无效)
assert(ymd.ok()); // 如果无效,assert 会触发
抛异常示例:
auto result = ymd + months{1};
if (!result.ok()) {std::ostringstream os;os << ymd << " + " << months{1}.count() << " months results in " << result;throw std::domain_error(os.str());
}
自动调整到月底:
ymd += months{1};
if (!ymd.ok())ymd = ymd.year() / ymd.month() / last; // 修正到该月最后一天,例如 2015-02-28
转换为 day_point 处理(不自动修正):
ymd += months{1};
if (!ymd.ok())ymd = day_point{ymd}; // 转成 day_point(天数计数),保持原值
4. 总结
- 库只负责创建日期和提供
ok()
判断函数,不自动纠错或抛异常。 - 这让你能写出更高效、可控的代码,决定在哪些地方要做有效性检测。
- 编译期会捕获一些无效字段顺序等错误,运行时的日期有效性由你自己检测和处理。
- 库本身不抛异常,但你的代码可以自由抛异常。
- 这种设计更适合需要底层高性能和灵活性的场景。
这部分讲的是日期类型中的特殊标记 last
,总结理解如下:
1. 什么是 last
?
last
是一个特殊的“日期”标记,表示某个月或某年某月的最后一天。- 在任何可以写“日(day)”的地方,都可以写
last
。
例如:
auto ymd1 = 2015_y / feb / last; // 2015年2月的最后一天,即2015-02-28
auto ymd2 = feb / last / 2015_y; // 顺序不同,但语义相同
auto ymd3 = last / feb / 2015_y; // 依然合法
2. 类型和API
- 表达式结果是
year_month_day_last
类型,而不是普通的year_month_day
。 year_month_day_last
的API几乎与year_month_day
相同。- 它也可以隐式转换成
year_month_day
(即转成具体某一天的日期)。
3. 使用场景示例
当日期无效时,可以用 last
修正到当月最后一天:
ymd += months{1}; // 例如 ymd 是 2015-01-31,加一个月变成 2015-02-31(无效)
if (!ymd.ok()) {ymd = ymd.year() / ymd.month() / last; // 转成当月最后一天 2015-02-28
}
4. 总结
last
是库提供的一个语法糖,用于方便表达“某月的最后一天”。- 它简化了对无效日期的处理(比如月份天数不同)。
- 让代码更直观,也方便避免无效日期错误。
这段内容介绍了“Indexed weekdays(带索引的星期几)”的用法,理解如下:
1. Indexed Weekdays 是什么?
- 在任何能写“日(day)”的地方,也可以写“带索引的星期几”(比如“第4个星期五”)。
- 语法形式是:
weekday[index]
,例如fri[4]
表示“第4个星期五”。
2. 示例
auto ymwd1 = 2015_y / sep / fri[4]; // 2015年9月的第4个星期五
auto ymwd2 = sep / fri[4] / 2015_y; // 顺序不同,但含义相同
auto ymwd3 = fri[4] / sep / 2015_y; // 依然合法
- 这些表达式的类型是
year_month_weekday
。
3. 转换为具体日期(year_month_day)
- 可以将
year_month_weekday
转成普通日期year_month_day
:
year_month_day ymd{ymwd1};
cout << ymwd1 << '\n'; // 输出类似:2015/Sep/Fri[4]
cout << ymd << '\n'; // 输出具体日期:2015-09-25
4. 带索引的“最后一个星期几”
- 也可以使用
last
作为索引,表示“某月最后一个指定星期几”:
auto ymwd_last = 2015_y / sep / fri[last]; // 2015年9月最后一个星期五
- 类型为
year_month_weekday_last
。 - 同样可以转换为普通日期:
year_month_day ymd{ymwd_last};
cout << ymwd_last << '\n'; // 输出 2015/Sep/Fri[last]
cout << ymd << '\n'; // 输出具体日期,比如 2015-09-25
5. 总结
- Indexed weekdays 让你能方便表达“某月第N个星期几”或“某月最后一个星期几”。
- 类型安全且语法灵活,支持多种字段顺序。
- 可以轻松转换成具体的日期。
day_point 概念
- day_point 是基于天的时间点(time_point),分辨率为“天”。
- 它是
std::chrono::time_point
的一个类型别名,时间单位是天(days
)。 - 具体定义是:
using day_point = std::chrono::time_point<std::chrono::system_clock, days>;
- 它表示自 Unix 纪元时间(1970-01-01)以来经过的天数。
示例说明
day_point dp = sep/25/2015; // 将日期转换成 day_point 类型
cout << dp.time_since_epoch().count() << '\n';
输出:
16703
- 表示自 1970-01-01 至 2015年9月25日,共经过了 16,703 天。
day_point 的优势
- 支持高效的“基于天”的日期算术:
dp += days{2}; // 加2天
cout << dp.time_since_epoch().count() << '\n'; // 变成 16705
- 这样操作比操作具体年月日字段更高效,也避免了日期边界问题。
总结
- day_point 是以天为单位的时间点类型,基于
<chrono>
标准库。 - 它是这套日期库的核心,负责处理日期的底层计数和计算。
- 你可以用它做高效的日期加减等算术运算。
system_clock::time_point 转换为 day_point
day_point
是一个时间点,时间单位是天,属于 coarse duration(粗粒度时间)。- 要将
system_clock::time_point
转成day_point
,可以用:
1. 使用 time_point_cast
day_point dp = std::chrono::time_point_cast<days>(std::chrono::system_clock::now());
- 缺点:
time_point_cast
是向零舍入(truncate toward zero), - 这对 1970 年之前的日期(负时间点) 会产生意料之外的错误结果。
2. 使用 floor
day_point dp = floor<days>(std::chrono::system_clock::now());
floor
类似time_point_cast
,但是是向负无穷舍入(round down),- 对负时间点(1970 年之前的时间)表现更正确。
示例
day_point dp = floor<days>(std::chrono::system_clock::now());
std::cout << dp.time_since_epoch().count() << '\n';
输出:
16703
- 表示从 1970-01-01 到现在(示例日期)过去了 16703 天。
总结
- 转换时推荐用
floor<days>()
,避免负时间点舍入错误。 - 这是标准库
<chrono>
里的时间点转换好方法。
day_point 和时间点(time_point)结合的日期时间操作
day_point
本质上是一个以天为单位的std::chrono::time_point
。
示例说明
auto tp = day_point{jan/3/1970};
assert(tp.time_since_epoch() == days{2});
jan/3/1970
转为day_point
,是从1970-01-01起的第2天(因为1970-01-01是第0天)。
可以给 day_point
加上小时、分钟、秒
auto tp = day_point{jan/3/1970} + 7h;
assert(tp.time_since_epoch() == 55h);
auto tp2 = day_point{jan/3/1970} + 7h + 33min;
assert(tp2.time_since_epoch() == 3333min);
auto tp3 = day_point{jan/3/1970} + 7h + 33min + 20s;
assert(tp3.time_since_epoch() == 200000s);
- 这里时间单位变得更细(小时、分钟、秒),
time_since_epoch()
会以相应单位表示。
从带有时分秒的 time_point
中恢复日期(day_point)
auto dp = floor<days>(tp3);
assert(dp.time_since_epoch() == days{2});
- 使用
floor<days>
截断时分秒,得到对应的日期(只到天的精度)。
获取当天时间(时分秒)
auto s = tp3 - dp;
assert(s == 27200s); // 7h33m20s 转换成秒数
tp3
减去日期部分dp
,得到当天的时间段。
将秒数分解为时分秒字段
auto time = make_time(s);
assert(time.hours() == 7h);
assert(time.minutes() == 33min);
assert(time.seconds() == 20s);
- 使用
make_time
将时间段转换成小时、分钟、秒。
将 day_point
转成年月日类型
auto ymd = year_month_day{dp};
assert(ymd.year() == 1970_y);
assert(ymd.month() == jan);
assert(ymd.day() == 3_d);
- 方便地得到具体日期信息。
总结
- 这个库完美衔接了 C++14
<chrono>
的时间点和日历日期。 day_point
既能做日期算术,也能做时间加减(时分秒),并且能方便地转换为年月日等类型。- 这让日期时间处理既高效又直观。
性能成本分析 — 这个库的开销到底有多大?
背景
- 作者没有跑性能测试,而是直接对比了用“date.h”中复杂类型生成代码的汇编,与简单的C结构体写法生成的汇编代码。
- 结果显示,这些高级封装并没有增加任何额外的运行时成本!
示例1:构造年月日结构体
date.h版本(year_month_day)
date::year_month_day make_year_month_day(int y, int m, int d) {using namespace date;return year{y}/m/d;
}
对应的汇编大致是位操作组合三个字段。
传统C-like版本
struct YMD_4 {std::int16_t year;std::uint8_t month;std::uint8_t day;
};
YMD_4 make_YMD_4(int y, int m, int d) {return {static_cast<std::int16_t>(y),static_cast<std::uint8_t>(m),static_cast<std::uint8_t>(d)};
}
生成的汇编与上面几乎相同。
结论1:
- “date.h”的“花哨”API在编译后生成的代码和传统C结构体构造函数几乎一样快。
- 即“Cute API”零空间和时间开销!
示例2:时间点偏移(将epoch从2000-01-01切换到1970-01-01)
date.h版本
using time_point = std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>;
time_point shift_epoch(time_point t) {using namespace date;return t + (day_point{jan/1/2000} - day_point{jan/1/1970});
}
C-like版本
long shift_epoch(long t) {return t + 946684800; // 秒数常量
}
对应汇编几乎完全一样:
leaq 946684800(%rdi), %rax
表示将常数偏移加到传入参数。
结论2:
- 用“date.h”封装的高层时间点算术,不会导致额外运行时负担。
- 编译器优化后,与裸指针加常数一样高效。
总结
- 该日期库设计巧妙,利用C++的类型系统和表达能力,完全没有牺牲性能。
- 你写的代码看起来简洁、类型安全、功能丰富,底层机器码同样简洁高效。
- 不用担心抽象导致运行慢,放心用!
“date.h” 的时间点偏移函数性能解析
代码示例对比
// date.h 版本,使用类型安全的时间点和日期
using time_point = std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>;
time_point shift_epoch(time_point t) {using namespace date;return t + (day_point{jan/1/2000} - day_point{jan/1/1970});
}
vs
// 传统 C-like 版本,直接用整数秒数
long shift_epoch(long t) {return t + 946684800;
}
关键点
day_point{jan/1/2000}
和day_point{jan/1/1970}
其实是两个日期的序列号(自1970年1月1日起的天数)。- 这两个日期相差 10,957 天(2000年1月1日距离1970年1月1日的天数)。
- 将天数转成秒数时,等于 10,957 × 86400 = 946,684,800 秒。
- 这些计算全部在编译期完成(
constexpr
),运行时只做简单加法。
运行时效果
- 编译器生成的机器码与直接加一个常量的C-like代码完全一样高效。
- 这保证了使用“date.h”库的类型安全和表达力,不会带来任何运行时开销。
总结
- 库利用了现代C++的编译期计算(
constexpr
)机制。 - 高层抽象代码在底层被优化成极简机器码。
- 类型安全和性能兼得,使用该库既安全又高效。
这段内容在对比几个C++日期时间库处理“每月第5个星期五”这类较复杂日期计算时的易用性、性能和代码规模,我帮你总结和拆解一下:
Inter-library Comparison — 不同库处理“每月第5个星期五”活动
任务背景:
- 要找到**每年中所有“有第5个星期五的月份”**的具体日期。
- 这类情况每年发生4~5次。
- 目标:
- 代码实现的简易性
- 运行时的性能消耗(执行时间)
- 库本身的代码规模
统一的接口设计(测试基准)
struct ymd {std::int16_t y;std::uint8_t m;std::uint8_t d;
};
std::pair<std::array<ymd, 5>, std::uint32_t> fifth_friday(int y);
- 返回值包含最多5个日期和实际找到的数量。
测试环境
- 编译开启
-O3
优化。 - 测试机器:4核MacBook Pro。
- 测量:平均多次运行时间(微秒级)。
- 输出示例:
2015-1-30
2015-5-29
2015-7-31
2015-10-30
<运行时间纳秒>
Bloomberg bdlt
- 通过静态查表来快速得出结果。
- 查表大小固定,有14条预定义模式。
- 速度快(平均几微秒内完成)。
- 代码大小中等(几十KB范围)。
- 方案本质上是查表代替计算,非常高效。
Boost date_time(传统版本)
- 通过循环12个月,调用库函数计算每月第5个星期五。
- 计算量中等,且调用外部函数。
- 代码相对较大(更多模板和函数调用)。
- 速度比Bloomberg略慢,但仍在微秒范围。
Boost date_time v2(新版)
- 类似Boost date_time,接口稍改进。
- 功能同样是按月循环调用计算。
- 性能和代码规模接近传统Boost。
Howard Hinnant’s date.h 库(你提到的date)
- 使用表达式式的语法,结合类型安全日期和时间点。
- 允许生成无效日期(例如不存在的第5个星期五),然后用
.ok()
检测。 - 代码最简洁,调用表达式清晰。
- 代码规模很小(几十KB以下)。
- 性能在所有库中表现优秀,甚至更快一点。
- 灵活性好,允许无效日期操作,简化实现。
重要对比点
库 | 代码规模 | 执行速度 | 易用性 | 特点 |
---|---|---|---|---|
Bloomberg bdlt | 中等 (~几十KB) | 极快 (<5μs) | 需要查表维护 | 静态查表,极致优化 |
Boost date_time | 较大 (~百KB) | 快 (~5-8μs) | 复杂,依赖Boost | 计算驱动,代码量大 |
Boost date_time v2 | 较大 | 类似传统 | 类似传统 | 新接口,未来可期 |
date.h (Howard) | 小 (~几KB) | 非常快 | 简洁易用 | 类型安全,灵活,支持无效日期 |
总结
- date.h 兼具简洁易用和高性能,且代码体积小。
- 它允许“无效日期”先生成,后验证,有利于写更自然的代码,减少分支判断。
- Bloomberg 的查表方案性能最好,但灵活性较低,需要维护表数据。
- Boost 方案更传统,但代码庞大且运行时成本稍高。
- 这体现了现代C++库设计趋向于类型安全和表达力强,同时保持高效。
1)用C标准库 <time.h>
判断2001年7月4日是星期几
#include <stdio.h>
#include <time.h>
static const char *const wday[] = {"星期日", "星期一", "星期二", "星期三","星期四", "星期五", "星期六", "-未知-"
};
int main() {struct tm time_str = {0};time_str.tm_year = 2001 - 1900; // 年份从1900算起time_str.tm_mon = 7 - 1; // 月份0开始计数time_str.tm_mday = 4;time_str.tm_isdst = -1; // 让系统自动判断夏令时if (mktime(&time_str) == (time_t)(-1)) {time_str.tm_wday = 7; // 计算失败,标记为未知}printf("%s\n", wday[time_str.tm_wday]);return 0;
}
运行后输出:
星期三
- 这里用
mktime
把日期转换成时间戳,会自动填充tm_wday
字段,代表星期几(0是星期日,6是星期六)。
2)用现代C++库date.h
(Howard Hinnant写的)
#include "date.h"
#include <iostream>
int main() {using namespace date;std::cout << weekday{2001_y/jul/4} << '\n'; // 输出 Wed
}
- 代码更简洁、易读。
- 输出同样是星期三(Wed)。
3)编译期判断(date.h
支持)
#include "date.h"
int main() {using namespace date;static_assert(weekday{2001_y/jul/4} == wed);
}
- 这是在编译阶段就判断2001年7月4日确实是星期三,编译不通过就报错。
- 很适合做编译期的日期校验。
4)时区处理 tz.h
默认之前的方法都在UTC时区下计算,实际工作中经常需要用本地时间:
#include "date/tz.h"
#include <iostream>
int main() {using namespace date;using namespace std::chrono;auto zone = locate_zone("America/Los_Angeles"); // 选择时区auto now = floor<milliseconds>(system_clock::now());auto local = zone->to_local(now);std::cout << now << " UTC\n";std::cout << local.first << " " << local.second << "\n"; // 本地时间和时区缩写
}
示例输出:
2015-09-25 16:50:06.123 UTC
2015-09-25 09:50:06.123 PDT
- 支持完整的IANA时区数据库,包括历史变动和夏令时切换。
- 还支持闰秒计算。
总结表格
方法 | 结果(2001年7月4日星期几) | 特点 |
---|---|---|
C标准库 | 星期三 | 运行时判断,跨平台 |
date.h 库 | 星期三 | 现代C++,代码简洁 |
date.h 编译期 | 星期三 | 编译期校验,安全 |
tz.h + date.h | 本地时区时间 | 支持完整时区和夏令时处理 |