提问人:Howard Hinnant 提问时间:11/14/2023 最后编辑:Howard Hinnant 更新时间:11/17/2023 访问量:2848
什么时候可以用 C++20 计时日期的 !ok() ?
When is it ok to be !ok() with C++20 chrono dates?
问:
该库允许日期静默地进入 !ok() 状态。例如:<chrono>
#include <chrono>
#include <iostream>
int
main()
{
using namespace std;
using namespace chrono;
auto date = 2023y/October/31;
cout << date.ok() << '\n';
date += months{1};
cout << date.ok() << '\n';
}
输出:
1
0
我知道 10 月 31 日是有效日期,而 11 月 31 日不是。但是,为什么 11 月 31 日不是错误(断言或抛出)呢?或者为什么它没有像其他日期库那样回弹到 11 月 30 日,或者滚动到 12 月 1 日?
难道就这样让 11 月 31 日默默存在不容易出错吗?
答:
在某种程度上,这个问题几乎不言自明:
或者为什么它没有像其他日期库那样回弹到 11 月 30 日,或者滚动到 12 月 1 日?
因为对于应该发生什么没有任何一致的做法,所以应该发生什么由客户决定。因此,从字面上看,程序员可以梦想和实现的任何事情都可能发生。<chrono>
例如,下面介绍如何快速回到 11 月 30 日:
auto date = 2023y/October/31;
date += months{1};
if (!date.ok())
date = date.year()/date.month()/last;
以下是进入 12 月的方法:
auto date = 2023y/October/31;
date += months{1};
if (!date.ok())
date = sys_days{date};
前者在代码中的作用非常明显。但后者值得进一步解释:转换为只是将数据结构转换为数据结构。即使 day 字段溢出,也允许进行这种转换。这就像 和 之间的 C 计时 API 一样。sys_days
{year, month, day}
{count_of_days}
tm
time_t
就像没有无效一样,也没有无效。这只是自 1970-01-01 以来(或之前)的天数。当您将该计数转换回 时,它会产生一个有效的 () 。time_t
sys_days
{year, month, day}
.ok() == true
year_month_day
显然,程序员也可以声明一个错误:
auto date = 2023y/October/31;
date += months{1};
if (!date.ok())
throw "oops!";
这种行为的一个很酷的方面(在程序员添加更正之前)是日期算术遵循正常的算术规则:
z = x + y;
assert(x == z - y);
也就是说,如果你加一个月,然后减去一个月,你保证得到相同的日期:
auto date = 2023y/October/31;
assert( date.ok());
date += months{1};
assert(!date.ok());
date -= months{1};
assert( date.ok());
assert(date == 2023y/October/31);
有时,正确的操作不是标志错误、翻转或回弹。有时正确的做法是忽略无效日期!
考虑这个问题:
我想找到某个年份的所有日期,即每月的第 5 个星期五(因为那是派对日或其他什么)。这是一个非常有效的函数,它收集了一年中所有 5个星期五:y
#include <array>
#include <chrono>
#include <utility>
#include <iostream>
std::pair<std::array<std::chrono::year_month_day, 5>, unsigned>
fifth_friday(std::chrono::year y)
{
using namespace std::chrono;
constexpr auto nan = 0y/0/0;
std::array<year_month_day, 5> dates{nan, nan, nan, nan, nan};
unsigned n = 0;
for (auto d = Friday[5]/January/y; d.year() == y; d += months{1})
{
if (d.ok())
{
dates[n] = year_month_day{d};
++n;
}
}
return {dates, n};
}
int
main()
{
using namespace std::chrono;
auto current_year = year_month_day{floor<days>(system_clock::now())}.year();
auto dates = fifth_friday(current_year);
std::cout << "Fifth Friday dates for " << current_year << " are:\n";
for (auto i = 0u; i < dates.second; ++i)
std::cout << dates.first[i] << '\n';
}
输出示例:
Fifth Friday dates for 2023 are:
2023-03-31
2023-06-30
2023-09-29
2023-12-29
事实证明,每年都有 4 个月或 5 个月,其中有 5 个星期五,这是一个不变的。因此,我们可以有效地将结果返回为对<数组<year_month_day,5>,无符号>,其中对的第二个成员将始终是 4 或 5。
第一项工作只是用一堆 s 初始化数组。我任意选择了一个良好的初始化值。我喜欢这个值的哪些方面?我喜欢的一件事是它是!如果我不小心访问了 时,额外的安全是结果是 .因此,能够在没有断言或异常的情况下构造这些值非常重要(就像 nan 一样)。成本?无。这些是编译时常量。year_month_day
0y/0/0
!ok()
.first[4]
.second == 4
year_month_day
!ok()
!ok()
接下来,我将迭代一年中的每个月。首先要做的是构建 1 月份的第 5 个星期五。然后将循环增加 1 个月,直到年份发生变化。y
现在,由于不是每个月都有第 5个星期五,因此这可能不会产生有效日期。但是在这个函数中,对构造无效日期的正确响应既不是断言也不是异常,也不是回弹或滚动。正确的反应是忽略日期并迭代到下个月。如果是有效日期,则继续推送结果。
在执行此程序期间计算了许多无效日期。它们都不代表错误。甚至在计算中使用了无效日期(增加一个月)。但是因为算术是有规律的,所以一切都有效。
因此,总而言之,最好将无效日期的行为留给聪明的程序员。因为聪明的程序员可以为他们的问题创建各种巧妙的解决方案,只要这样做的灵活性。
评论
class ok_date
std::chrono
assert(date.ok())
0y/0/0
nan
nad
评论