Home > Enterprise >  Parsing std::chrono::duration with boost::program_options
Parsing std::chrono::duration with boost::program_options

Time:01-12

I'm trying to parse the command line using boost::program_options (boost version 1.80.0, and Apple clang 14.0.0 on arm64-apple-darwin22.2.0) in order to read a std::chrono::duration<int> object formatted as 12h34m56s. According to the documentation, one is required to overload the validate function as follows in my code below.

#include <boost/program_options.hpp>
#include <iostream>

namespace po = boost::program_options;
using Duration = std::chrono::duration<int>;

inline void validate(boost::any &value, const std::vector<std::string> &values, Duration* target_type) {
    using namespace std::chrono_literals;
    try {
        po::validators::check_first_occurrence(value);
        auto duration_str = po::validators::get_single_string(values);
        Duration duration = Duration::zero();
        std::string::size_type i = 0;
        while (i < duration_str.size()) {
            std::string::size_type j = i;
            while (j < duration_str.size() && std::isdigit(duration_str[j])) {
                  j;
            }
            int v = std::stoi(duration_str.substr(i, j - i));
            i = j;
            if (i < duration_str.size() && duration_str[i] == 'h') {
                duration  = v * 1h;
                  i;
            } else if (i < duration_str.size() && duration_str[i] == 'm') {
                duration  = v * 1min;
                  i;
            } else if (i < duration_str.size() && duration_str[i] == 's') {
                duration  = v * 1s;
                  i;
            }
        }
        value = boost::any(duration);
    } catch (...) {
        throw po::invalid_option_value("Invalid duration");
    }
}
int main(int ac, char *av[])
{
    try
    {
        po::options_description desc("Allowed options");
        desc.add_options()
            ("help,h", "produce a help screen")
            ("duration,d", po::value<Duration>(), "duration in 12h34m56s format")
            ;

        po::variables_map vm;
        po::store(po::parse_command_line(ac, av, desc), vm);
        if (vm.count("help"))
        {
            std::cout << desc;
            return 0;
        }
        if (vm.count("duration"))
        {
            std::cout << "The duration is \""
                 << vm["duration"].as<Duration>().count()
                 << "\"\n";
        }
    }
    catch (std::exception& e)
    {
        std::cout << e.what() << "\n";
    }

    return 0;
}

However, this fails to compile, and the compiler reports that:

/opt/homebrew/include/boost/lexical_cast/detail/converter_lexical.hpp:243:13: error: static_assert failed due to requirement 'has_right_shift<std::istream, std::chrono::duration<int, std::ratio<1, 1>>, boost::binary_op_detail::dont_care>::value || boost::has_right_shift<std::wistream, std::chrono::duration<int, std::ratio<1, 1>>, boost::binary_op_detail::dont_care>::value' "Target type is neither std::istream`able nor std::wistream`able"
            BOOST_STATIC_ASSERT_MSG((result_t::value || boost::has_right_shift<std::basic_istream<wchar_t>, T >::value),
            ^                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

I've also tried implementing the istream operator>> as well as overloading boost::lexical_cast for the std::chrono::duration<int> class but with no success. What am I missing here?

Edit after @rubenvb 's answer

I tried making std::chrono::duration<int> both std::istream and std::wistream able, but again to no avail. Note that std::chrono::from_stream is not available on my compiler.

template <typename String>
inline Duration parseDuration(const String& duration_str)
{
    using namespace std::chrono_literals;
    Duration duration;
    typename String::size_type i = 0;
    while (i < duration_str.size()) {
        std::wstring::size_type j = i;
        while (j < duration_str.size() && std::iswdigit(duration_str[j])) {
              j;
        }
        int v = std::stoi(duration_str.substr(i, j - i));
        i = j;
        if (i < duration_str.size() && duration_str[i] == 'h') {
            duration  = v * 1h;
              i;
        } else if (i < duration_str.size() && duration_str[i] == 'm') {
            duration  = v * 1min;
              i;
        } else if (i < duration_str.size() && duration_str[i] == 's') {
            duration  = v * 1s;
              i;
        }
    }
    return duration;
}
inline std::wistream& operator>>(std::wistream& is, Duration& duration) {
    std::wstring duration_str;
    is >> duration_str;
    duration = parseDuration(duration_str);
    return is;
}

inline std::istream& operator>>(std::istream& is, Duration& duration) {
    std::string duration_str;
    is >> duration_str;
    duration = parseDuration(duration_str);
    return is;
}

CodePudding user response:

You can overload operator>>. Keep in mind it relies on ADL, though, so it needs ot be in std::chrono namespace.

However, that's icky, as it will either lead to surprises to other code or even risk ODR violations across TUs.

Instead, notice that validate also leverages ADL. Finally note that your overload can be in any of the associated namespaces: std (due to vector and basic_string), std::chrono (due to duration) but also boost(due toany`)!This vastly reduces the potential to interfere with current or future standard symbols.

So here's fixed:

Live On Coliru

#include <boost/program_options.hpp>
#include <chrono>
#include <iostream>

namespace po = boost::program_options;
using Duration = std::chrono::duration<int>;

namespace boost {

    template <class CharT>
    void validate(boost::any& value, std::vector<std::basic_string<CharT>> const& values, Duration*, int) {
        using namespace std::chrono_literals;
        try {
            po::validators::check_first_occurrence(value);
            auto                   duration_str = po::validators::get_single_string(values);
            Duration               duration     = Duration::zero();
            std::string::size_type i            = 0;
            while (i < duration_str.size()) {
                std::string::size_type j = i;
                while (j < duration_str.size() && std::isdigit(duration_str[j])) {
                      j;
                }
                int v = std::stoi(duration_str.substr(i, j - i));
                i     = j;
                if (i < duration_str.size() && duration_str[i] == 'h') {
                    duration  = v * 1h;
                      i;
                } else if (i < duration_str.size() && duration_str[i] == 'm') {
                    duration  = v * 1min;
                      i;
                } else if (i < duration_str.size() && duration_str[i] == 's') {
                    duration  = v * 1s;
                      i;
                }
            }
            value = boost::any(duration);
        } catch (...) {
            throw po::invalid_option_value("Invalid duration");
        }
    }

} // namespace boost

int main(int argc, char** argv) {
    po::options_description opts("Demo");
    opts.add_options()                                //
        ("duration,d", po::value<Duration>(), "test") //
        ;

    std::cout << opts << "\n";

    po::variables_map vm;
    store(po::parse_command_line(argc, argv, opts), vm);

    if (vm.contains("duration")) {
        std::cout << "Value: " << vm["duration"].as<Duration>() << "\n";
    }
}

Prints e.g.

g   -std=c  20 -O2 -Wall -pedantic -pthread main.cpp -lboost_program_options
./a.out -d 3m
Demo:
  -d [ --duration ] arg test

Value: 180s

CodePudding user response:

It seems the error you're getting is that you don't have an operator>> defined for std::wistream (i.e. the std::basic_istream<wchar_t> in your error message).

Additionally, you can extract a std::chrono::duration from a stream using from_stream. So that can easily replace your manual parsing code.

  • Related