boostboost-program-optionsstdoptional

Using boost::program_options with std::optional


Boost's program_options library now supports boost::optional, can the same be done with std::optional?

I attempted to modify both the documentation example and the code in the PR, but neither seems to work.

For example, the very simple case for integers (before trying template specializations):

void validate(boost::any& v, const std::vector<std::string>& values, std::optional<int>* target_type,
              int) {
  using namespace boost::program_options;
  validators::check_first_occurrence(v);
  const string& s = validators::get_single_string(values);

  int n = lexical_cast<int>(s);
  v = any(std::make_optional<int>(n));
}

fails with the error that the target type is not istreamable:

external/boost/boost/lexical_cast/detail/converter_lexical.hpp:243:13: 
error: static_assert failed due to requirement 
'has_right_shift<std::__1::basic_istream<char>, std::__1::optional<int>, boost::binary_op_detail::dont_care>::value || boost::has_right_shift<std::__1::basic_istream<wchar_t>, std::__1::optional<int>, boost::binary_op_detail::dont_care>::value'
"Target type is neither std::istream`able nor std::wistream`able"

Solution

  • The problem with things like validate (and operator>> as well) is often ADL¹.

    You need to declare the overload in one of the associated namespaces. In this case, because int is a primitive type, the only associated namespaces come from library code:

    You'd prefer not to add your code in those namespaces, because you might interfere with library functions or invite future breakage when the library implementation details change.

    If you had to choose, you'd prefer to choose boost here, because

    Sidenote: Keep an eye out for tag_invoke - a better way to build customization points in libraries


    The Fix

    After all this verbiage, the solution is very simple:

    namespace boost {
        void validate(boost::any& v, const std::vector<std::string>& values,
                      std::optional<int>*, int) {
            using namespace boost::program_options;
            validators::check_first_occurrence(v);
            const std::string& s = validators::get_single_string(values);
    
            int n = boost::lexical_cast<int>(s);
            v     = boost::any(std::make_optional<int>(n));
        }
    } // namespace boost
    

    Adding two lines made it work: Live On Wandbox.

    Other Notes:

    1. The "solution" injecting operator>> in general is less pure because

      • it has a potential to "infect" all other code with ADL-visible overloads that might interfere. Way more code uses operator>> than boost's validate function
      • it thereby invites UB due to ODR violations, when another translation unit, potentially legitimely, defines another operator>> for the same arguments.
    2. On recent compilers you can say vm.contains instead of the slightly abusive vm.count

    3. There's another snag with non-streamable types, where, if you define a default value, you probably also need to specify the string representation with it.

    Listing

    Compiling on Compiler Explorer

    #include <boost/program_options.hpp>
    #include <optional>
    #include <iostream>
    
    namespace po = boost::program_options;
    
    namespace boost {
        void validate(boost::any& v, const std::vector<std::string>& values,
                    std::optional<int>*, int) {
            using namespace boost::program_options;
            validators::check_first_occurrence(v);
            const std::string& s = validators::get_single_string(values);
    
            int n = boost::lexical_cast<int>(s);
            v     = boost::any(std::make_optional<int>(n));
        }
    } // namespace boost
    
    int main(int ac, char* av[]) {
        try {
            using Value = std::optional<int>;
    
            po::options_description desc("Allowed options");
            desc.add_options()
                ("help", "produce help message")
                ("value", po::value<Value>()->default_value(10, "10"),
                    "value")
            ;
    
            po::variables_map vm;
            po::store(po::parse_command_line(ac, av, desc), vm);
            po::notify(vm);
    
            if (vm.contains("value")) {
                std::cout << "value is " << vm["value"].as<Value>().value() << "\n";
            }
    
        } catch (std::exception& e) {
            std::cout << e.what() << "\n";
            return 1;
        }
    }
    

    BONUS

    As an added exercise, let's demonstrate that if your optional value_type is not a primitive, but rather your library type, declared in a namespace MyLib, then we don't have most of the trade-offs above:

    namespace MyLib {
        template <typename T> struct MyValue {
            MyValue(T v = {}) : value(std::move(v)) {}
    
          private:
            T value;
            friend std::istream& operator>>(std::istream& is, MyValue& mv) {
                return is >> mv.value;
            }
            friend std::ostream& operator<<(std::ostream& os, MyValue const& mv) {
                return os << mv.value;
            }
        };
    

    Now you could provide generic validators for any types in your MyLib namespace, be it optional or not, and have ADL find them through your MyLib namespace:

        template <typename T, typename Values>
        void validate(boost::any& v, Values const& values, T*, int) {
            po::validators::check_first_occurrence(v);
            v = boost::lexical_cast<T>(
                    po::validators::get_single_string(values));
        }
    
        template <typename T, typename Values>
        void validate(boost::any& v, Values const& values, std::optional<T>*, int) {
            po::validators::check_first_occurrence(v);
            v = std::make_optional(
                    boost::lexical_cast<T>(
                        po::validators::get_single_string(values)));
        }
    } // namespace MyLib
    

    See Live Demo

    #include <boost/program_options.hpp>
    #include <iostream>
    #include <iomanip>
    
    namespace po = boost::program_options;
    
    namespace MyLib {
        template <typename T> struct MyValue {
            MyValue(T v = {}) : value(std::move(v)) {}
    
          private:
            T value;
            friend std::istream& operator>>(std::istream& is, MyValue& mv) {
                return is >> std::boolalpha >> mv.value;
            }
            friend std::ostream& operator<<(std::ostream& os, MyValue const& mv) {
                return os << std::boolalpha << mv.value;
            }
        };
    
        // Provide generic validators for any types in your MyLib namespace, be it
        // optional or not
        template <typename T, typename Values>
        void validate(boost::any& v, Values const& values, T*, int) {
            po::validators::check_first_occurrence(v);
            v = boost::lexical_cast<T>(
                    po::validators::get_single_string(values));
        }
    
        template <typename T, typename Values>
        void validate(boost::any& v, Values const& values, std::optional<T>*, int) {
            po::validators::check_first_occurrence(v);
            v = std::make_optional(
                    boost::lexical_cast<T>(
                        po::validators::get_single_string(values)));
        }
    } // namespace MyLib
    
    int main(int ac, char* av[]) {
        try {
            using Int    = MyLib::MyValue<int>;
            using OptInt = std::optional<MyLib::MyValue<int>>;
            using OptStr = std::optional<MyLib::MyValue<std::string> >;
    
            po::options_description desc("Allowed options");
            desc.add_options()
                ("ival", po::value<Int>()->default_value(Int{10}),
                      "integer value")
                ("opti", po::value<OptInt>()->default_value(OptInt{}, "(nullopt)"),
                      "optional integer value")
                ("sval", po::value<OptStr>()->default_value(OptStr{"secret"}, "'secret'"),
                      "optional string value")
            ;
    
            po::variables_map vm;
            po::store(po::parse_command_line(ac, av, desc), vm);
            po::notify(vm);
    
            std::cout << "Options: " << desc << "\n";
    
            if (vm.contains("ival")) {
                std::cout << "ival is " << vm["ival"].as<Int>() << "\n";
            }
            if (vm.contains("opti")) {
                if (auto& v = vm["opti"].as<OptInt>())
                    std::cout << "opti is " << v.value() << "\n";
                else
                    std::cout << "opti is nullopt\n";
            }
            if (vm.contains("sval")) {
                if (auto& v = vm["sval"].as<OptStr>())
                    std::cout << "sval is " << v.value() << "\n";
                else
                    std::cout << "sval is nullopt\n";
            }
        } catch (std::exception& e) {
            std::cout << e.what() << "\n";
            return 1;
        }
    }
    

    For ./a.out --ival=42 --sval=LtUaE prints:

    Options: Allowed options:
      --ival arg (=10)        integer value
      --opti arg (=(nullopt)) optional integer value
      --sval arg (='secret')  optional string value
    
    ival is 42
    opti is nullopt
    sval is LtUaE
    

    ¹ see also See also Why Does Boost Use a Global Function Override to Implement Custom Validators in "Program Options"