Search code examples
c++boost

Boost program_options with boost::optional


I am trying to write an CLI using boost::program_options to an existing codebase that utilizes a lot of boost::optional arguments so I would like to parse boost::optional's from the command line. If it is not specified, the result is boost::none, and if it is specified I get an initialized value.

When I attempt to do this using a custom Boost validator, I get a bad_any_cast. Below is a MCVE of the problem.

I have a class

class MyClass {                                                                                                     
public:                                                                                                       
    int x;                                                                                                    
    MyClass(const int a) : x(a) {};                                                                                                                                                                                                                                                                                              
};    

and a custom Boost validator for this class. This style of validator was taken straight from the boost docs.

void validate(boost::any& v, const std::vector<std::string>& values,                                          
          MyClass* target_type, int) {                                                                          

    v = boost::any(boost::optional<MyClass>(boost::in_place(1)));                                                   
}

Lastly, my main function, which creates a simple parser.

#include <boost/program_options.hpp>                                                                          
#include <boost/optional.hpp>                                                                                 
#include <boost/optional/optional_io.hpp>                                                                     
#include <boost/utility/in_place_factory.hpp> 

int main(int argc, char** argv) {                                                                             
    po::options_description desc("");                                                                         
    desc.add_options()                                                                                        
        ("MyClass", po::value<boost::optional<MyClass>>()->default_value(boost::none, ""), "MyClass");        

    po::variables_map args;                                                                                   

    po::store(po::parse_command_line(argc, argv, desc), args);                                                

}   

When I do not pass the --MyClass options on the command line, the code runs successfully. However, if I pass the --MyClass option, I get a bad_any_cast

terminate called after throwing an instance of 'boost::exception_detail::clone_impl<boost::exception_detail::error_info_injector<boost::bad_any_cast> >'
  what():  boost::bad_any_cast: failed conversion using boost::any_cast

I stepped through with GDB and this is being thrown in the MyClass any_cast, yet if I write similar code outside of boost::program_options, it works succesfuly.

For example, the following code, that casts the same boost::optional to boost::any and then casts it back, runs without error.

#include <iostream>                                                                                           
#include <boost/program_options.hpp>                                                                          
#include <boost/optional.hpp>                                                                                 
#include <boost/optional/optional_io.hpp>                                                                     
#include <boost/utility/in_place_factory.hpp>                                                                 

namespace po = boost::program_options;                                                                        

class MyClass {                                                                                              
public:                                                                                                       
    int x;                                                                                                    
    MyClass(const int a) : x(a) {};                                                                           
};                                                                                                            

int f(boost::any& v) {                                                                                        
    v = boost::any(boost::optional<MyClass>(boost::in_place(1)));                                             
}                                                                                                             


int main(int argc, char** argv) {                                                                             

    boost::any v;                                                                                             
    f(v);                                                                                                     
    boost::any_cast<boost::optional<MyClass>>(v);                                                             
}    

I know that program_options supports default_value so I could use an if statement to wrap the base value in an optional after parsing but I think it would be much cleaner to achive this using the custom validator approach above.

Does anyone have any ideas or suggestions on how to go about fixing this?


Solution

  • The validation function does not take an optional. This is implied by the fact that the type argument (target_type) is MyClass*, not optional<MyClass>*. docs

    The function takes four parameters. The first is the storage for the value, and in this case is either empty or contains an instance of the magic_number class. The second is the list of strings found in the next occurrence of the option. The remaining two parameters are needed to workaround the lack of partial template specialization and partial function template ordering on some compilers.

    Here's my take on it:

    Live On Coliru

    #include <boost/optional.hpp>
    #include <boost/optional/optional_io.hpp>
    #include <boost/program_options.hpp>
    #include <boost/utility/in_place_factory.hpp>
    #include <iostream>
    #include <string>
    
    namespace po = boost::program_options;
    
    struct MyClass {
        int x;
        MyClass(int a) : x(a){};
    
        friend std::ostream& operator<<(std::ostream& os, MyClass const& mc) {
            return os << "MyClass(" << mc.x << ")";
        }
    };
    
    void validate(boost::any &v, const std::vector<std::string> &values, MyClass * /*target_type*/, int) {
        v = MyClass(std::stoi(values.front()));
    }
    
    int main(int argc, char **argv) {
        po::options_description desc("");
        desc.add_options()("MyClass", po::value<boost::optional<MyClass> >()->default_value(boost::none, ""), "MyClass");
    
        po::variables_map args;
        store(parse_command_line(argc, argv, desc), args);
        notify(args);
    
        std::cout << "Arg: " << args["MyClass"].as<boost::optional<MyClass> >() << "\n";
    }
    

    For:

    ./a.out --MyClass 42
    ./a.out --MyClass no_chill
    ./a.out
    

    Prints

    + ./a.out --MyClass 42
    Arg:  MyClass(42)
    + ./a.out --MyClass no_chill
    terminate called after throwing an instance of 'std::invalid_argument'
      what():  stoi
    + ./a.out
    Arg: --
    

    Bonus

    Taking a hints from the docs I think you could make it more elegant:

    int main(int argc, char **argv) {
        po::options_description desc("");
    
        boost::optional<MyClass> my_option;
        desc.add_options()("MyClass", po::value(&my_option), "MyClass");
    
        po::variables_map args;
        store(parse_command_line(argc, argv, desc), args);
        notify(args);
    
        std::cout << "Arg: " << my_option << "\n";
    }
    

    Removing all the error-prone repeating of types and names. Note: this does not work for boost versions lower than 1.65, and the user will see a static assert error coming from "lexical_cast.hpp" when compiling (see release notes for 1.65).

    Live On Coliru

    Still the same output.