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.
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<>
.
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.
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:
#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'
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