Search code examples
c++c++-clic++-chrono

Convert steady_clock::time_point (C++) to System::DateTime (C++/CLI)


I need to convert C++ std::chrono::steady_clock::time_point to C++/CLI System::DateTime.

Background:
I am wrapping a C++ library with a C++/CLI interface, to be used by a .NET app.
One of the C++ methods return a std::chrono::steady_clock::time_point. I thought it is appropriate to returns a System::DateTime from the C++/CLI wrapper method. Thus the need to convert.

I am aware that if I had a system_clock::time_point, I could have converted it to time_t as explained here. Then I could have used DateTimeOffset.FromUnixTimeMilliseconds, and from it get a System::DateTime.
Another approach could have been to use time_since_epoch.
But neither to_time_t nor time_since_epoch are available for std::chrono::steady_clock (about time_since_epoch see here).

However - I cannot change the C++ interface.
Also didn't manage to properly convert steady_clock::time_point to e.g. system_clock::time_point.

The solution I came up with:
I take current time from both std::chrono::steady_clock and System::DateTime, then calculate offset from the steady_clock::time_point, and finally apply this offset in reverse to the DateTime time. I calculate the offset in milliseconds, and since the precision I am interested in is of seconds, it works well.
This method is shown in the code below.

But it feels a bit awkward. It is also sensitive to the requested precision.

My question: can you suggest a better way to do the conversion ?

using namespace System;
#include <chrono>

System::DateTime SteadyClockTimePointToDateTime(std::chrono::steady_clock::time_point const & tCPP)
{
    auto nowCPP = std::chrono::steady_clock::now();
    auto nowCLI = System::DateTime::Now;
    long long milliSecsSinceT = std::chrono::duration_cast<std::chrono::milliseconds>(nowCPP - tCPP).count();
    System::DateTime tCLI = nowCLI - System::TimeSpan::FromMilliseconds(static_cast<double>(milliSecsSinceT));
    return tCLI;
}

int main(array<System::String ^> ^args)
{
    System::Console::WriteLine("System::DateTime::Now (for debug): " + System::DateTime::Now.ToString()); // print reference time for debug 
    auto tCPP = std::chrono::steady_clock::now();   // got the actual value from a C++ lib.

    System::Threading::Thread::Sleep(100); // pass some time to simulate stuff that was executed since the time_point was received.

    System::DateTime tCLI = SteadyClockTimePointToDateTime(tCPP);
    System::Console::WriteLine("System::DateTime (converted):      " + tCLI.ToString()); // should show a time very close to System::DateTime::Now above
    return 0;
}

Output example:

System::DateTime::Now (for debug): 23-May-22 16:41:04
System::DateTime (converted):      23-May-22 16:41:04

Note: I added the C++ tag because the question is not a pure C++/CLI issue. E.g. there might be a solution involving conversion between std::chrono clocks that will enable an easy further conversion to System::DateTime (as mentioned above regarding DateTimeOffset.FromUnixTimeMilliseconds).


Solution

  • The approach is sound, but the code can be made shorter and easier to read with a couple small changes:

    template<typename Rep, typename Period>
    System::TimeSpan DurationToTimeSpan(std::chrono::duration<Rep, Period> const& input)
    {
         auto milliSecs =
                  std::chrono::duration_cast<std::chrono::milliseconds>(input).count();
         return System::TimeSpan::FromMilliseconds(milliSecs);
    }
    
    System::DateTime SteadyClockTimePointToDateTime(
                              std::chrono::steady_clock::time_point const & tCPP)
    {
        auto const nowCPP = std::chrono::steady_clock::now();
        auto nowCLI = System::DateTime::Now;
        auto tCLI = nowCLI + DurationToTimeSpan(tCPP - nowCPP);
    }
    

    The specific changes made are:

    1. Hide use of the duration cast and TimeSpan factory function invocation in another helper function.1
    2. Reverse the order of subtraction of nowCPP and tCPP, to avoid having to reverse the sign later.
    3. Add const to local variables having native type and which will not be changed. .NET types sadly are not const-correct, because const-ness is only respected by the C++/CLI compiler and not the languages in which the .NET library is written.
    4. Avoid explicit type conversion between the return value of count() and the parameter of AddMilliseconds. If for some platform there is a different duration representation and implicit conversion doesn't work, it's better to have the compiler tell the maintenance programmer.

    Note that the result of this function does NOT provide the "steady clock" guarantee. In order to do so, one should generate a single time_point/DateTime pair and save it for later reuse.


    1 Another choice would be to use the AddMilliseconds member function, which replaces a call to TimeSpan factory function and overloaded operator-:

    System::DateTime SteadyClockTimePointToDateTime(
                              std::chrono::steady_clock::time_point const & tCPP)
    {
        auto const nowCPP = std::chrono::steady_clock::now();
        auto nowCLI = System::DateTime::Now;
        auto const milliSecsUntilT =
            std::chrono::duration_cast<std::chrono::milliseconds>(tCPP - nowCPP).count();
        auto tCLI = nowCLI.AddMilliseconds(milliSecsUntilT);
        return tCLI;
    }