1

I'm obtaining from a magical gremlin an array of non-negative integer values in memory, say of type I. Each of these represents a number of days since some epoch (which is many years before 0 in the ISO 8601 calendar).

Now, I want to wrap these values - essentially through pointer or reinterpretation - by C++ classes, which can interact well with the code of std::chrono, or perhaps I should say be used with code which normally takes std::chrono timepoints and durations.

What would be my best course of action for this? Note I don't want to replace any data in the array nor create a new value for each of the existing ones, elsewhere.

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • 1
    "*since the epoch*" *Which* epoch? – Nicol Bolas Jul 09 '19 at 20:24
  • 1
    C++ has *strict* aliasing rules so more than likely you'll not be able to treat `I` as something you can add to a time point. – NathanOliver Jul 09 '19 at 20:26
  • You will need to convert your non-negative integers into a calendar that can cover the range of all dates/times that those integers can represent. The clocks from `std::chrono` (`system_clock`, `stead_clock`, etc..) deal in seconds from some fixed point in time. `system_clock` is the only one related to wall-clock time. (with its origin being hardware dependent, but generally seconds from Jan 1 1970) *"which is many years before 0 in the ISO 8601 calendar"* -- which [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Dates)? – David C. Rankin Jul 09 '19 at 21:00
  • @NathanOliver: If the data members of a class are the same, I think I'm safe with punning the type. Aren't I? – einpoklum Jul 09 '19 at 21:02
  • @DavidC.Rankin: The `std::chrono` classes take the clock as a template; I may be able to provide a clock with the appropriate resolution. – einpoklum Jul 09 '19 at 21:04
  • @einpoklum C++ doesn't really do type punning. It's allowed if both objects are standard layout, both have the same members in the same order and they are members of union. It is also legal to treat a `complex` as a `double[2]` because the standard explicitly mandates it to be. Outside of that I'm not sure if there is any other legal type punning. – NathanOliver Jul 09 '19 at 21:06
  • @einpoklum - you will definitely want to look at [Handling Julian dates in C++11/14](https://stackoverflow.com/questions/33964461/handling-julian-dates-in-c11-14) which explains what it appears you need to do. (as well as the answer's author's github offerings) – David C. Rankin Jul 09 '19 at 21:14
  • @NathanOliver you can `reinterpret_cast` back to the original type and, if I am not mistaken, you can `reinterpret_cast` to `char` and use that (but don't quote me on that, as I am not sure :p) – bolov Jul 09 '19 at 21:15
  • @bolov That's not really punning though. Going there and back leaves you with what you already had which the OP had. I did forget to include the `char*` case but that is really only useful in copying and that isn't what the OP wants to do. – NathanOliver Jul 09 '19 at 21:20

2 Answers2

7

Each of these represents a number of days since the epoch (which is many years before 0 in the ISO 8601 calendar).

Let's say you're referring to the epoch for the Julian Day Number. This epoch is consistent with your description. The link says this epoch is November 24, 4714 BC, in the proleptic Gregorian calendar. Instead of using the "BC" system, I find it convenient to use a system with negative years instead so that there is a smooth mathematical transition across the year 0. In this system the epoch is November 24, -4713.

Using Howard Hinnant's free, open-source, header-only date library, this is very easy to do. If I've got the wrong epoch, just substitute in the correct one in the obvious place.

#include "date/date.h"
#include <iostream>

date::sys_days
to_sys_days(int i)
{
    using namespace date;
    return sys_days{days{i} -
        (sys_days{1970_y/January/1} - sys_days{-4713_y/November/24})};
}

The date::sys_days return type is a std::chrono::time_point<std::chrono::system_clock, std::chrono::duration<int, std::ratio<86400>>>. Or in English: it is a system_clock-based time_point with a precision of days. This time_point will implicitly convert to your platform's system_clock::time_point.

Now you can pass your int to to_sys_days, and pass the result to a function taking a system_clock::time_point. For example:

void
display(std::chrono::system_clock::time_point tp)
{
    using date::operator<<;
    std::cout << tp << '\n';
}

int
main()
{
    display(to_sys_days(2'458'674));
}

This outputs:

2019-07-09 00:00:00.000000

to_sys_days is a very cheap operation. You can afford to do it each time you read a single element of the data. All it does is subtract 2440588 from i. The optimized machine code for to_sys_days (clang++ -O3) is literally:

leal    -2440588(%rdi), %eax

I.e. all of the type-changing business happens at compile-time. It's free. The only thing that happens at run-time is the epoch offset adjustment. This is the bare minimum that must be done no matter what to align your epoch with the system_clock epoch.

So if you have an array of int as your data, you don't have to make a copy of the entire array. You just transform each element of it on demand. For example:

int
main()
{
    int data[] = {2'458'674, 2'458'675, 2'458'676, 2'458'677, 2'458'678};
    for (auto i : data)
        display(to_sys_days(i));
}

Output:

2019-07-09 00:00:00.000000
2019-07-10 00:00:00.000000
2019-07-11 00:00:00.000000
2019-07-12 00:00:00.000000
2019-07-13 00:00:00.000000

If you don't want to use the date library, you can still get the job done, it is just a bit more work.

First create a duration type that means days:

using days = std::chrono::duration
    <int, std::ratio_multiply<std::ratio<24>, std::chrono::hours::period>>;

Then figure out the number of days between 1970-01-01 and your epoch.

Then take your integral value i, wrap it in days, and subtract off your epoch difference. Then you can construct a system_clock::time_point with your value of days.

Note: It is important that you do the epoch adjustment in units of days as opposed to converting first to the units of system_clock::time_point and then doing the adjustment. This latter strategy will overflow on some platforms. You are protected from overflow if you do the epoch offset in days precision.


I strongly advise against using the reinterpret_cast tool to get this job done. It seems both unnecessary and dangerous.


Update

I forgot about the part that says the Julian Day epoch is noon instead of midnight. If you want to take this into account, it is very easy with the date lib:

auto
to_sys_days(int i)
{
    using namespace date;
    using namespace std::chrono;
    return sys_time<hours>{days{i} -
        (sys_days{1970_y/January/1} - sys_days{-4713_y/November/24} - 12h)};
}

I simply subtracted 12 hours off the epoch difference and let auto deduce the return type for me (which is now a system_clock-based time_point with a precision of hours).

The exact same display function with the same input now outputs:

2019-07-09 12:00:00.000000
Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
-1

If it's really just about the duration then you can simply do

std::chrono::days(integer_value);

and you will get a chrono duration type.

Stephan Dollberg
  • 32,985
  • 16
  • 81
  • 107
  • 1. This will not work, since you're using the wrong epoch. 2. I need to be able reinterpret_cast, not construct. – einpoklum Jul 09 '19 at 21:02
  • 1
    @einpoklum due to strict aliasing rules you cannot `reinterpret_cast` between them. For instance `struct A { int x; }; struct B {int x; }`. You cannot `reinterpret_cast` between `A` and `B`, neither between `A` and `int`. (well, you can, but it's invalid to use the result; the only valid thing you can do is `reinterpret_cast` to the original type and use that, which is pointless in your situation). – bolov Jul 09 '19 at 21:12
  • @einpoklum: Pointers or values, you cannot access an `A` through a pointer to a `B`, and vice-versa. That's what the strict aliasing rule says. – Nicol Bolas Jul 09 '19 at 22:23
  • @NicolBolas: 1. In that case, I'm afraid I'll have to ignore that rule and rely on compilers doing what my intuition says is the right thing. 2. But if `A::x` is is an `int` and so is `B::x`, doesn't that effectively mean that, strict aliasing rule notwithstanding, it is actually necessary for the reinterpretation to work? After all, the code for `struct A` and `struct B` methods cannot really make any differing assumptions. – einpoklum Jul 09 '19 at 22:26
  • @NicolBolas: Or, let me put it this way, at least for my case: On machines using the kind of physical memory that's almost ubiquitous on desktops, servers and laptops, there is no way for the compiled code to distinguish between a plain integer and an integer which was constructed and used as part of an `A::x`. – einpoklum Jul 09 '19 at 22:32
  • @einpoklum: Or you could just copy it, like most people would do. It's just an integer; it's not going to kill your performance. – Nicol Bolas Jul 09 '19 at 22:33
  • @NicolBolas: Sorry, can't copy the whole array. No space for that. I need it to be an array of something else (with the same data members), and that's that. – einpoklum Jul 09 '19 at 22:36
  • @einpoklum the strict aliasing rule has some consequences. E.g. if you have a function receiving as parameters `A* p1, B* p2` the compiler can assume that modifying the object pointed by `p2` will not modify the object pointed by `p1` so if the body of the function is `p1->x = 11; p2->x = 24; return p1->x` the compiler can (and they do) generate code for that function that will return `11`. If you reinterpret_cast `A*` as `B*` and pass that as argument you can see how this will be a problem. This is just a practical example. My advice: don't write illegal code. https://godbolt.org/z/U6j3ZJ – bolov Jul 10 '19 at 06:57