TLDR #
Add return value in the lambda function in store_into
template <typename T, typename std::enable_if<
std::is_integral<T>::value>::type* = nullptr>
auto& store_into(T& var) {
if (m_default_value.has_value()) {
var = std::any_cast<T>(m_default_value);
}
action([&var](const auto& s) {
var = details::parse_number<T, details::radix_10>()(s);
return var;
});
return *this;
}
Address #
https://github.com/p-ranav/argparse/issues/385
Compile #
#include <argparse/argparse.hpp>
int main(int argc, char* argv[]) {
argparse::ArgumentParser program("cool binary");
size_t num_threads;
program.add_argument("--threads")
.help("Number of threads to use")
.metavar("N")
.scan<'u', size_t>()
.required()
.default_value<size_t>(1)
.store_into(num_threads)
;
program.add_argument("--boxes")
.help("Number of boxes to use")
.metavar("B")
.required()
.scan<'u', size_t>()
.default_value(size_t(1));
try {
program.parse_args(argc, argv);
} catch (const std::exception& err) {
std::cerr << err.what() << std::endl;
std::cerr << program;
return EXIT_FAILURE;
}
size_t num_boxes = program.get<size_t>("--boxes");
std::cout << "got num_threads = " << num_threads
<< " num_boxes = " << num_boxes << std::endl;
}
build
cmake_minimum_required(VERSION 3.20)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
project(issue_argparse)
include_directories(include)
add_executable(main main.cpp)
execute
> ./build/main --threads 5 --boxes 3
and then throw error
--threads: no value provided.
Debug #
Where is the bug? #
The error message come from the function throw_required_arg_no_value_provided_error
, and this function is only used in function validate
. Let’s step into this function.
void validate() const {
if (m_is_optional) {
if (!m_is_used && !m_default_value.has_value() && m_is_required) {
throw_required_arg_not_used_error();
}
if (m_is_used && m_is_required && m_values.empty()) {
throw_required_arg_no_value_provided_error(); // error occur here!
}
}
// ...
}
It seems that the reason for the bug is m_values.empty()
. Next, we need to find the reason why the m_values.empty()
equals to true
.
Function validate
will be called in the execution of program.parse_args
. By adding breakpoint to function program.parse_args
, i found something odd in the following code
std::visit(ActionApply{start, end, *this}, m_action);
- When the arg is
--boxes
, them_action
is recognized asvalued_action
- When the arg is
--threads
, them_action
is recognized asvoid_action
When the arg is recognized asvalued_action
, them_values
will be modified by the following code.
std::transform(first, last, std::back_inserter(self.m_values), f);
Therefore, the m_values.empty()
is false
in arg --boxes
, while the m_values.empty()
is true
in arg --threads
. This is the reason for the bug.
Why the arg --threads
and --boxes
is different?
#
The following code is the definition of valued_action
and void_action
using valued_action = std::function<std::any(const std::string &)>;
using void_action = std::function<void(const std::string &)>;
From the definition, i found that there is only one different between the two action, which is the return type. It doesn’t seem to be helpful for debugging. As the code mentioned, the only different between arg --threads
and --boxes
is the call of store_into
. Let’s step into this function:
template <typename T, typename std::enable_if<std::is_integral<T>::value>::type * = nullptr>
auto &store_into(T &var) {
// ...
action([&var](const auto &s) {
var = details::parse_number<T, details::radix_10>()(s);
});
// ...
}
The core of the function store_into
is action
, let’s step into it.
template <class F, class... Args>
auto action(F &&callable, Args &&... bound_args)
-> std::enable_if_t<std::is_invocable_v<F, Args..., std::string const>,
Argument &> {
using action_type = std::conditional_t<
std::is_void_v<std::invoke_result_t<F, Args..., std::string const>>,
void_action, valued_action>;
// ...
return *this;
}
Function action
first needs to define the action_type. This hateful code can be rewritten as:
using invoke_result = std::invoke_result_t<F, Args..., std::string const>;
constexpr auto is_void = std::is_void_v<invoke_result>;
using action_type = std::conditional_t<is_void, void_action, valued_action>;
It means that:
if invoke_result == void then
action_type = void_action
else
action_type = valued_action
Back to the function store_into
action([&var](const auto& s) {
var = details::parse_number<T, details::radix_10>()(s);
});
The parameter of action
have no return value, so the action_type
will be recognized as void_action
. After adding the return value, the program can exit successfully.
action([&var](const auto& s) {
var = details::parse_number<T, details::radix_10>()(s);
return var;
});
Execute result:
> ./build/main --threads 5 --boxes 4
got num_threads = 5 num_boxes = 4
Why the arg --boxes
have no bug?
#
This is beacuse the default action_type
of m_action
is valued_action
. The action_type
will change into void_action
when you call store_into
.
std::variant<valued_action, void_action> m_action{
std::in_place_type<valued_action>,
[](const std::string& value) { return value; }};
Other type? #
When use other partial specialization of store_into
, the error occur again. Adding return value for all store_into
can solve it.