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)