boostc++17metaprogrammingc++-chrono

Is there a simple way to make the units of a std::chrono::duration configurable?


I'm attempting to allow setting the units that a std::chrono::duration is reported in by modifying an xml config file parsed by boost::property_tree. My current, non-compiling solution attempts to do this using std::variant.

In .hpp class declaration

using TimestampVariant = std::variant<
        std::chrono::nanoseconds, 
        std::chrono::microseconds,
        std::chrono::milliseconds,
        std::chrono::seconds
    >; 

TimestampVariant _timestamp_v;

in .cpp

auto GetTimestampVisitor =  [](const auto& t) -> decltype(auto) {
    return std::chrono::duration_cast<std::remove_reference_t<decltype(t)>>(std::chrono::system_clock::now().time_since_epoch()).count(); 
};

void SetupFunction()
{
    boost::property_tree::ptree property_tree;
    boost::property_tree::read_xml(filepath, property_tree);
    auto config = property_tree.get_child("Config");
    std::string timestamp_type = config.get<std::string>("ReportingUnits");
    if(!timestamp_type.compare("seconds") || !timestamp_type.compare("s"))
    {
        _timestamp_v = std::chrono::seconds();
    }
    else if(!timestamp_type.compare("milliseconds") || !timestamp_type.compare("ms"))
    {
        _timestamp_v = std::chrono::milliseconds();
    }
    else if(!timestamp_type.compare("microseconds") || !timestamp_type.compare("us"))
    {
        _timestamp_v = std::chrono::microseconds();
    }
    else if(!timestamp_type.compare("nanoseconds") || !timestamp_type.compare("ns"))
    {
        _timestamp_v = std::chrono::nanoseconds();
    }
}

void OutputFunction()
{
   std::cout << std::visit(GetTimestampVisitor, _timestamp_v) << std::endl;
}

I'm admittedly not skilled at metaprogramming. Is there a simpler way to do this? Essentially I can only guarantee at compile time that the type of the duration will be one of a subset of duration types.


Solution

  • Meta-programming is not required because there is a common representation.

    Just always store a plain duration with enough precision. Then on output scale to the desired presentation format:

    Live On Coliru

    #include <boost/property_tree/xml_parser.hpp>
    #include <chrono>
    #include <iostream>
    #include <map>
    
    struct Program {
        using duration = std::chrono::steady_clock::duration;
    
        auto ReportingDuration(duration d) const {
            using namespace std::chrono_literals;
            using unit = decltype(1.ns); // or `duration` for integral
    
            static std::map<std::string_view, unit> const units = {
                {"ns", 1ns}, {"nanoseconds", 1ns},  //
                {"us", 1us}, {"microseconds", 1us}, //
                {"ms", 1ms}, {"milliseconds", 1ms}, //
                {"s", 1s},   {"seconds", 1s},       //
                {"m", 1min}, {"minutes", 1min},     //
                {"h", 1h},   {"hours", 1h},         //
                {"d", 24h},  {"days", 24h},
            };
            return d / units.at(reporting_unit_);
        };
    
        bool ReportingUnitValid() const try {
            return ReportingDuration(std::chrono::hours(24)), true;
        } catch (...) {
            return false;
        }
    
        void SetupFunction(std::string const& filepath = "config.xml") {
            boost::property_tree::ptree pt;
            read_xml(filepath, pt);
            reporting_unit_ = pt.get<std::string>("Config.ReportingUnits");
            if (!ReportingUnitValid())
                throw std::runtime_error("Invalid reporting unit");
        }
    
        void OutputFunction() {
            using namespace std::chrono_literals;
            for (duration v : std::vector<duration> //
                 {                                  //
                  1ns, 1us, 1ms, 1s,                //
                  1min,                             //
                  1h, 1h + 1min,                    //
                  1h + 1min + 1s,                   //
                  24h, 24h + 1min, 24h + 1min + 1s})
                std::cout << std::setw(16) << v << ": " << ReportingDuration(v) << " " << reporting_unit_
                          << std::endl;
        }
    
      private:
        std::string reporting_unit_ = "s";
    };
    
    int main() {
        std::cout << std::fixed;
        Program p;
        p.SetupFunction();
        p.OutputFunction();
    }
    

    With all tests:

    g++ -std=c++20 -O2 -Wall -pedantic -pthread main.cpp 
    for unit in d h m s ms us ns; do cat > config.xml <<< "<Config><ReportingUnits>$unit</ReportingUnits></Config>"; ./a.out; done
    

    Printing:

                 1ns: 0.000000 d
              1000ns: 0.000000 d
           1000000ns: 0.000000 d
        1000000000ns: 0.000012 d
       60000000000ns: 0.000694 d
     3600000000000ns: 0.041667 d
     3660000000000ns: 0.042361 d
     3661000000000ns: 0.042373 d
    86400000000000ns: 1.000000 d
    86460000000000ns: 1.000694 d
    86461000000000ns: 1.000706 d
                 1ns: 0.000000 h
              1000ns: 0.000000 h
           1000000ns: 0.000000 h
        1000000000ns: 0.000278 h
       60000000000ns: 0.016667 h
     3600000000000ns: 1.000000 h
     3660000000000ns: 1.016667 h
     3661000000000ns: 1.016944 h
    86400000000000ns: 24.000000 h
    86460000000000ns: 24.016667 h
    86461000000000ns: 24.016944 h
                 1ns: 0.000000 m
              1000ns: 0.000000 m
           1000000ns: 0.000017 m
        1000000000ns: 0.016667 m
       60000000000ns: 1.000000 m
     3600000000000ns: 60.000000 m
     3660000000000ns: 61.000000 m
     3661000000000ns: 61.016667 m
    86400000000000ns: 1440.000000 m
    86460000000000ns: 1441.000000 m
    86461000000000ns: 1441.016667 m
                 1ns: 0.000000 s
              1000ns: 0.000001 s
           1000000ns: 0.001000 s
        1000000000ns: 1.000000 s
       60000000000ns: 60.000000 s
     3600000000000ns: 3600.000000 s
     3660000000000ns: 3660.000000 s
     3661000000000ns: 3661.000000 s
    86400000000000ns: 86400.000000 s
    86460000000000ns: 86460.000000 s
    86461000000000ns: 86461.000000 s
                 1ns: 0.000001 ms
              1000ns: 0.001000 ms
           1000000ns: 1.000000 ms
        1000000000ns: 1000.000000 ms
       60000000000ns: 60000.000000 ms
     3600000000000ns: 3600000.000000 ms
     3660000000000ns: 3660000.000000 ms
     3661000000000ns: 3661000.000000 ms
    86400000000000ns: 86400000.000000 ms
    86460000000000ns: 86460000.000000 ms
    86461000000000ns: 86461000.000000 ms
                 1ns: 0.001000 us
              1000ns: 1.000000 us
           1000000ns: 1000.000000 us
        1000000000ns: 1000000.000000 us
       60000000000ns: 60000000.000000 us
     3600000000000ns: 3600000000.000000 us
     3660000000000ns: 3660000000.000000 us
     3661000000000ns: 3661000000.000000 us
    86400000000000ns: 86400000000.000000 us
    86460000000000ns: 86460000000.000000 us
    86461000000000ns: 86461000000.000000 us
                 1ns: 1.000000 ns
              1000ns: 1000.000000 ns
           1000000ns: 1000000.000000 ns
        1000000000ns: 1000000000.000000 ns
       60000000000ns: 60000000000.000000 ns
     3600000000000ns: 3600000000000.000000 ns
     3660000000000ns: 3660000000000.000000 ns
     3661000000000ns: 3661000000000.000000 ns
    86400000000000ns: 86400000000000.000000 ns
    86460000000000ns: 86460000000000.000000 ns
    86461000000000ns: 86461000000000.000000 ns
    

    Live Demo:

    Note how the video only recompiles for fractional durations at the very end.