Search code examples
boostc++-chronoboost-program-options

Custom validate function to parse std::chrono::milliseconds via Boost program options


I am trying to parse in an option via Boost program options, which contain a time in [s] or [ms]. Currently, the variable is hard-coded as, using literals:

std::chrono::milliseconds timeout = 10s;

I am happy to define it as this in the config file

#time in [s]
timeout = 10

However I could not figure out how to do the validate function. This is what tried:

struct chrono_ms : public std::chrono::milliseconds {};

void validate(  boost::any& v,
                const std::vector<std::string>& values,
                chrono_ms*,
                int)
{
    // Make sure no previous assignment to 'v' was made.
    validators::check_first_occurrence(v);

    // Extract the first string from 'values'. If there is more than
    // one string, it's an error, and exception will be thrown.
    const std::string& s = validators::get_single_string(values);

    // Convert to std::chrono::milliseconds.
    v = std::chrono::milliseconds(std::stoi(s));
}   // validate() for std::chrono::milliseconds

Funnily enough I managed to write a validate function for a std::array, but I am not familiar with std::chrono and could not for the life of me figure out how to do this... any suggestions would be much appreciated, thanks.


Solution

  • Aside from the question (which isn't clear), the whole point of std::chrono is that you do not HAVE to know about the unit.

    Just make the argument a std::chrono::duration<>.

    Design issues

    First, I suspect the real problem you had was that your chrono_ms is not constructible. You need to inherit some constructors, like e.g.

    using clock = std::chrono::steady_clock;
    
    struct duration : public clock::duration {
        using clock::duration::duration;
    };
    

    Next up, there are issues deriving from standard library types that weren't design to be derived from. E.g., the type of 5 * chrono_ms(1) would not be chrono_ms but std::chrono::milliseconds.

    Also issues with implicit conversions (due to inheriting explicit constructors).

    For this reason, I'd suggest a simple wrapper instead:

    using clock = std::chrono::steady_clock;
    struct duration {
        clock::duration value;
    };
    

    This leads you to explicitly write what you mean, and not have surprises.

    Validate

    Next up, here's my suggestion for an option parser that takes the unit:

    template<class charT>
    void validate(boost::any& v, const std::vector< std::basic_string<charT> >& xs, duration*, long)
    {
        po::validators::check_first_occurrence(v);
        std::basic_string<charT> s(po::validators::get_single_string(xs));
    
        int magnitude;
        clock::duration factor;
    
        namespace qi = boost::spirit::qi;
        qi::symbols<char, clock::duration> unit;
        unit.add("s",1s)("ms",1ms)("us",1us)("µs",1us)("m",1min)("h",1h);
    
        if (parse(s.begin(), s.end(), qi::int_ >> (unit|qi::attr(1s)) >> qi::eoi, magnitude, factor))
            v = duration {magnitude * factor};
        else
            throw po::invalid_option_value(s);
    }
    

    You do not need to put that into the boost or program_options namespace. ADL will find it (that was presumably the whole reason you wanted a "strong" typedef like your chrono_ms).

    A test program:

    Live On Wandbox

    #include <boost/program_options.hpp>
    #include <boost/spirit/include/qi.hpp>
    #include <chrono>
    #include <vector>
    #include <iostream>
    
    namespace po = boost::program_options;
    using namespace std::chrono_literals;
    
    namespace myns {
        using clock = std::chrono::steady_clock;
        struct duration {
            clock::duration value;
    
            friend std::ostream& operator<<(std::ostream& os, duration const& holder) {
                using namespace std::chrono;
                auto ms = duration_cast<milliseconds>(holder.value).count();
    
                if (ms >= 1000)
                    return os << (ms/1000) << "s";
                else
                    return os << ms << "ms";
            }
        };
    
        template<class charT>
        void validate(boost::any& v, const std::vector< std::basic_string<charT> >& xs, duration*, long)
        {
            po::validators::check_first_occurrence(v);
            std::basic_string<charT> s(po::validators::get_single_string(xs));
    
            int magnitude;
            clock::duration factor;
    
            namespace qi = boost::spirit::qi;
            qi::symbols<char, clock::duration> unit;
            unit.add("s",1s)("ms",1ms)("us",1us)("µs",1us)("m",1min)("h",1h);
    
            if (parse(s.begin(), s.end(), qi::int_ >> unit >> qi::eoi, magnitude, factor))
                v = duration {magnitude * factor};
            else
                throw po::invalid_option_value(s);
        }
    
    }
    
    int main() {
        po::options_description options;
        options.add_options()
            ("duration,d", po::value<myns::duration>(), "duration (e.g. 1s or 10ms)");
    
        char const* tests[][3] = {
            { "", "-d", "1s" },
            { "", "-d", "2200us" },
            { "", "-d", "10ms" },
            { "", "-d", "5m" },
            { "", "-d", "24h" },
            // 
            { "", "-d", "s" }, // invalid
            { "", "-d", "5" }, // invalid
        };
    
        for (auto args : tests) try {
            std::copy(args, args +3, std::ostream_iterator<std::string>(std::cout << "Test ", " "));
            auto parsed = po::parse_command_line(3, args, options);
            po::variables_map vm;
            po::store(parsed, vm);
            po::notify(vm);
    
            std::cout << "\tduration=" << vm["duration"].as<myns::duration>() << "\n";
        } catch (std::exception const& e) {
            std::cout << "\t" << e.what() << "\n";
        }
    }
    

    Prints

    Test  -d 1s     duration=1s
    Test  -d 2200us     duration=2ms
    Test  -d 10ms   duration=10ms
    Test  -d 5m     duration=300s
    Test  -d 24h    duration=86400s
    Test  -d s  Error 'the argument ('s') for option '--duration' is invalid'
    Test  -d 5  Error 'the argument ('5') for option '--duration' is invalid'
    

    BONUS

    If you, e.g. wanted to make a certain unit the default, replace unit in the parser expression with e.g. (unit|qi::attr(1s)):

    Test  -d 1s     duration=1s
    Test  -d 2200us     duration=2ms
    Test  -d 10ms   duration=10ms
    Test  -d 5m     duration=300s
    Test  -d 24h    duration=86400s
    Test  -d s  the argument ('s') for option '--duration' is invalid
    Test  -d 5  duration=5s