Argparse Issue 385

Argparse Issue 385.

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, the m_action is recognized as valued_action
  • When the arg is --threads, the m_action is recognized as void_action When the arg is recognized as valued_action, the m_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.