Why does `std::chrono::weekday` allow but doesn't preserve values outside of valid range?

Viewed 204

According to section [time.cal.wd.overview]/1 of the C++ standard:

weekday represents a day of the week in the civil calendar. It normally holds values in the range 0 to 6, corresponding to Sunday through Saturday, but it may hold non-negative values outside this range.

At the same time arithmetic operations perform modulo 7 arithmetic forcing the result in the range [0, 6], e.g.

weekday wd(7);
// wd.ok() == false - wd is invalid
++wd; // wd == weekday(1)
// wd.ok() == true  - wd silently becomes valid

Why does weekday have such peculiar behavior, particularly why are values outside of [0, 6] allowed but not preserved by arithmetic operations?

3 Answers

The weekday(unsigned wd) constructor promises to hold any value in the range [0, 255]. The rationale for this is:

  1. It's very fast.
  2. It allows a client to assign an "unused" value to something useful in the client's logic.

For an example of (2):

constexpr weekday not_a_weekday{255};
...
weekday wd = not_a_weekday;
in >> wd;
if (wd == not_a_weekday)
    throw "oops";

weekday arithmetic forces the range back into [0, 6] because if you write the algorithm to do modulo 7 arithmetic, with no range checking at all, this is what naturally happens. I.e. this is the fastest thing to do.


So in summary: Performance is the rationale for the current weekday spec, combined with a sizeof which is as small as possible (which can also contribute to performance).

However given as much performance as possible, whatever behaviors are left over (occur naturally) can be beneficial to standardize and let clients take advantage of these behaviors as opposed to saying that they are Undefined Behavior TM.

Indeed, the spec avoids UB as much as possible, opting for Unspecified Behavior instead. For example weekday{300} may not store the value you want, but it can not reformat your disk, and the optimizer is not allowed to pretend the code doesn't exist.

Why does weekday have such peculiar behavior, particularly why are values outside of [0, 6] allowed but not preserved by arithmetic operations?

Any arithmetic operation on weekday will potentially overflow. So you have to do a modulo operation there. It's important that sat + days{1} == sun, since that's what everyone expects. And you really want (sat + days{1}).ok() to hold as well, since surely that's valid. And it's just as important that sat + days{8} == sun and sat + days{701} == sun, and so forth. That's just how calendar arithmetic works.

So it's pretty necessary to do a modulo 7 on all the arithmetic operations. There's just no point in holding 8 as a weekday value - that's not really a valid weekday.

On the other hand, doing the modulo on construction doesn't have as much clear value. It's extra work that you may not need to do, and it might even hide bugs. What if you want to just validate weekday(user_input).ok()? Would everyone that needs to know what to check externally?

In short, values above 6 are allowed on construction because that makes sense to allow, but arithmetic doesn't preserve them because it doesn't make sense to preserve them.

But Howard is a frequent contributor to SO, so he'll probably chime in with a better answer.

It's not so strange.

If you give it an out-of-range value, then that is your mistake. It would not be fair to force us all to endure endless bounds checking just to catch this rare occurrence.

Similarly, it is not fair to expect the well-defined operations of this type to do anything in particular on values that are expressly considered out of range, nor is it immediately clear what sort of deterministic result you expect from such an operation. Which day comes after "ooglebooglebargleday"?

You'll find this philosophy a lot throughout C++. You don't pay for what you don't use, and you can shoot yourself in the foot as long as you don't mind having to carry yourself the rest of the way home.

Related