3

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;
}
Aamir
  • 1,974
  • 1
  • 14
  • 18
marital_weeping
  • 618
  • 5
  • 18

3 Answers3

2

You can overload operator>>. Keep in mind it relies on ADL, though, so it needs to 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
sehe
  • 374,641
  • 47
  • 450
  • 633
  • I didn't review your parsing code. Perhaps use a parser generator to make it easier to verify/maintain. I just noticed this eerily similar answer by a guy you might recognize https://stackoverflow.com/questions/45071699/custom-validate-function-to-parse-stdchronomilliseconds-via-boost-program-op – sehe Jan 11 '23 at 17:10
  • Interestingly, while your standalone code runs as expected, I can't seem to run it in the context of my code -- i get exactly the same error when I place `validate()` inside the boost namespace. What did work though was putting the `>>` overloads in the `std::chrono` namespace. I wonder why that is? – marital_weeping Jan 11 '23 at 17:29
  • Because you didn't copy my code? I suspect you missed the other (subtle) differences. – sehe Jan 11 '23 at 17:32
1

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.

rubenvb
  • 74,642
  • 33
  • 187
  • 332
0

I think a better approach is to add a specialization of lexical_cast or try_lexical_convert method from boost conversion library.

The code in program options calls internally boost::lexical_cast<> (from the conversion lib) to convert between text and the desired type, and the cast uses internally try_lexical_convert.

This way you don't have to repeat the boilerplate validate code (i.e., things like check_first_occurrence and get_single_string) every time you need to support yet another type.

Also, if you want to set default value and output options_description, then you also have to add conversion from duration to string.

Have a look at an example code that I use to set timeouts in a config file (hence it supports only number followed by us/ms/s units):

#include <boost/program_options.hpp>
#include <chrono>
#include <string>
#include <cstdlib>
#include <regex>

namespace boost::conversion::detail {
    template<>
    bool try_lexical_convert<std::chrono::microseconds, std::string>(const std::string& in, std::chrono::microseconds& out){
        std::smatch m;
        if(!std::regex_match(in, m, std::regex(R"((\d+(?:\.\d*)?)\s*([um]?)s)",std::regex_constants::icase)))
            return false;
        
        std::string number = m.str(1);
        std::string unit   = m.str(2);
        
        char * end;
        double count = std::strtod(number.data(), &end);
        if(number.data() + number.length() != end)
            return false;
        
        if     (unit.empty()) count *= 1e6;
        else if(unit == "m" ) count *= 1e3;
        else if(unit != "u" ) assert(false);
        
        out = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::duration<double, std::micro>(count));
        
        return true;
    }

    template<>
    bool try_lexical_convert<std::string, std::chrono::microseconds>(const std::chrono::microseconds& in, std::string& out){
        out = std::to_string(in.count()) + "us";
        return true;
    }
}

#include <iostream>

int main(int argc, char *argv[]) {
    using namespace std::literals;
    using namespace boost::program_options;

    std::chrono::microseconds us;

    options_description opts;
    opts.add_options()("time,t", value(&us)->default_value(10ms), "some time");
    std::cout << opts;

    variables_map vm;
    store(parse_command_line(argc, argv, opts), vm);
    notify(vm);

    std::cout << us.count() << "us\n" << us.count()/1e3 << "ms\n" << us.count()/1e6 << "s" << std::endl;
    return 0;
}

(this should compile with C++17, and with minor changes it should work with C++11 as well)

Jan K
  • 73
  • 6