2

It's already known that Boost.Program_options does not handle multitoken options in config files (separated by spaces) conveniently like when dealing with command-line options. The main solution that I found involves defining custom validators. As such, I defined the following validator:

template<typename T>
void validate(boost::any& v, const std::vector<std::string>& values, std::vector<T>*, int) {
    std::cout << "validate called!" << std::endl;
    boost::program_options::validators::check_first_occurrence(v);
    if (values.size() == 0)
        throw boost::program_options::validation_error(boost::program_options::validation_error::kind_t::at_least_one_value_required);
    std::vector<T> numeric_values(values.size(), 0);
    std::transform(values.begin(), values.end(), numeric_values.begin(), [] (const std::string&& string) {
        std::istringstream iss(string);
        T val;
        iss >> val;
        if (iss.fail())
            throw boost::program_options::validation_error(boost::program_options::validation_error::kind_t::invalid_option_value, "", string);
        return val;
    });
    v = std::move(numeric_values);
}

However, when I try to specify an std::vector option in the config file, the validator never gets called and I get the standard error the argument ('4 8 16 32') for option 'sizes' is invalid. The relevant option line is this:

("sizes", po::value<std::vector<size_t>>(&sizes)->multitoken(), "Sizes of the different tensor modes, separated by spaces")

I suspect this solution might fail in this case because the option is of the type std::vector<T>, since I know this is handled as a special case by Boost.Program_options. I tried looking for more documentation on the use of custom validators, or check the source code, but couldn't find what I needed.

I can think of alternate solutions involving forwarding wrapper objects, which would be clearly different from the std::vector<T> type while simply forwarding the assignment operator. However, these objects would need to be passed when defining the options while still being in scope during the parsing, meaning I have to define a temporary variable for each std::vector<T> option, which doesn't seem like the best solution. I hope someone knows a better solution to this problem.

Update: I created a small reproducible example, showing that the problem only occurs with an std::vector option, not with the custom coordinate struct. Here's the source file:

#include <iostream>
#include <boost/program_options.hpp>
#include <vector>
#include <utility>
#include <fstream>

namespace po = boost::program_options;

struct coordinate {
    double x, y;
};

struct options_storage {
    coordinate coords = {0, 0};
    std::vector<int> vector = {};
};

void print_options_storage(const options_storage& options) {
    std::cout << "coords: (" << options.coords.x << ", " << options.coords.y << ")" << std::endl;
    std::cout << "vector: [";
    for (size_t i = 0; i < options.vector.size(); i++) {
        std::cout << options.vector[i];
        if (i < options.vector.size() - 1)
            std::cout << ", ";
    }
    std::cout << "]" << std::endl;
}

template<typename T>
void validate(boost::any& v, const std::vector<std::string>& values, std::vector<T>*, int) {
    std::cout << "validate-vector called!" << std::endl;
    boost::program_options::validators::check_first_occurrence(v);
        if (values.size() == 0)
                throw boost::program_options::validation_error(boost::program_options::validation_error::kind_t::at_least_one_value_required);
        std::vector<T> numeric_values(values.size(), 0);
        std::transform(values.begin(), values.end(), numeric_values.begin(), [] (const std::string&& string) {
        std::istringstream iss(string);
        T val;
        iss >> val;
        if (iss.fail())
            throw boost::program_options::validation_error(boost::program_options::validation_error::kind_t::invalid_option_value, "", string);
        return val;
    });
        v = std::move(numeric_values);
}

// From https://stackoverflow.com/questions/5884465/boostprogram-options-config-file-option-with-multiple-tokens
void validate(boost::any& v, const std::vector<std::string>& values, coordinate*, int) {
    std::cout << "validate-coordinate called!" << std::endl;
    coordinate c;
    std::vector<double> dvalues;
    for(std::vector<std::string>::const_iterator it = values.begin(); it != values.end(); ++it) {
        std::stringstream ss(*it);
        std::copy(std::istream_iterator<double>(ss), std::istream_iterator<double>(), std::back_inserter(dvalues));
        if(!ss.eof())
            throw po::validation_error(boost::program_options::validation_error::kind_t::invalid_option_value, "", *it);
    }
    if (dvalues.size() != 2)
        throw po::validation_error(boost::program_options::validation_error::kind_t::invalid_option_value, "", "");
    c.x = dvalues[0];
    c.y = dvalues[1];
    v = c;
}

int main(int argc, char** argv) {
    
    options_storage options;
    
    po::options_description desc("General");
    desc.add_options()
        ("coords", po::value<coordinate>(&options.coords)->multitoken())
        ("vector", po::value<std::vector<int>>(&options.vector)->multitoken())
    ;
    po::variables_map vm;
    
    po::store(po::parse_command_line(argc, argv, desc), vm);
    po::notify(vm);
    print_options_storage(options);
    
    std::ifstream ifs1;
    ifs1.open("config1.ini", std::ifstream::in);
    po::store(po::parse_config_file(ifs1, desc), vm);
    po::notify(vm);
    ifs1.close();
    print_options_storage(options);
    
    std::ifstream ifs2;
    ifs2.open("config2.ini", std::ifstream::in);
    po::store(po::parse_config_file(ifs2, desc), vm);
    po::notify(vm);
    ifs2.close();
    print_options_storage(options);
    
    return 0;
    
}

With the config files config1.ini:

coords = 3.5 4.5

config2.ini:

coords = 5.5 6.5
vector = 5 6 7 8

CMakeLists.txt:

cmake_minimum_required(VERSION 3.9)
project(boost-program-options-test LANGUAGES CXX)

set(CMAKE_VERBOSE_MAKEFILE on)

find_package(Boost COMPONENTS program_options)

add_executable(po-test test.cpp)
target_link_libraries(po-test stdc++)
target_link_libraries(po-test ${Boost_LIBRARIES})
target_compile_options(po-test PRIVATE -Wall -Wextra -Wno-unknown-pragmas -Wno-unknown-warning-option -march=native -mtune=native -O3 -DNDEBUG)
set_target_properties(po-test PROPERTIES
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/.."
)

Output:

coords: (0, 0)
vector: []
validate-coordinate called!
coords: (3.5, 4.5)
vector: []
terminate called after throwing an instance of 'boost::exception_detail::clone_impl<boost::exception_detail::error_info_injector<boost::program_options::invalid_option_value> >'
  what():  the argument ('5 6 7 8') for option 'vector' is invalid
Aborted (core dumped)
Wout12345
  • 91
  • 5
  • 1
    Thanks for your comment, I added one. – Wout12345 Jun 24 '21 at 07:54
  • Great - it's much better now. I have absolutely no experience with Boost `program_options`, but is a `std::vector` really a `multitoken`? There's only one `std::vector`. – Ted Lyngmo Jun 24 '21 at 08:04
  • Yes, it works fine when just passing the options through the command line. For example, `./po-test --coords 0.5 1.5 --vector 1 2 3 4` results in "validate-coordinate called! coords: (0.5, 1.5) vector: [1, 2, 3, 4] [...]". Boost.Program_options has some special case for handling `std::vector`, but it's probably this special case that's making it ignore my custom validator here. – Wout12345 Jun 24 '21 at 10:30

0 Answers0