Input Stream Operator lookup order with Boost.Program_options

754 Views Asked by At

I have an enumeration class and a corresponding input stream operator in the namespace fw::example.

#include <stdexcept>
#include <string>
#include <istream>

namespace fw {
namespace example {

enum class Booleans {
  kFalse = 0,
  kTrue = 1
};

std::istream& operator>>(std::istream& is, Booleans& boolean) {
  std::string token;
  is >> token;

  if ("true" == token) {
    boolean = Booleans::kTrue;
  } else if ("false" == token) {
    boolean = Booleans::kFalse;
  } else {
    throw std::invalid_argument{
        "Invalid string representation for an enumerator of the Booleans class."};
  }

  return is;
}

}  // namespace example
}  // namespace fw

Then I bind a variable of that enum via Boost.Program_options in the same namespace:

// [...]
description_.add_options()(
"bool",
boost::program_options::value<Booleans>(&boolean_)
    ->required()
    ->default_value(Booleans::kFalse),
"A boolean string, either true or false.");
// [...]

But I do not want to expose the std::runtime_error to the user, instead I want to use the proper exception from the framework, and that is boost::program_options::invalid_option_value. Imagine a more complex scenario in which the enum class and the input stream operator are defined in a library and I am unable to modify that library.

So I tried the following (according to an answer I found on SO, which I can not find again):

namespace boost {
namespace program_options {

std::istream& operator>>(std::istream& is,
                         fw::example::Booleans& boolean) {
  try {
    return fw::example::operator>>(is, boolean);
  } catch (std::invalid_argument const& kException) {
    throw invalid_option_value{kException.what()};
  }
}

}  // namespace program_options
}  // namespace boost

The whole code compiles, but the thing is, that only the free function fw::example::operator>> is called and therefore the wrong execption is propagated:

terminate called after throwing an instance of 'std::invalid_argument'
  what():  Invalid string representation for an enumerator of the Booleans class.

I am looking for a solution to be able to cleanly separate the Boost.Program_options related code from the rest, but also use the appropriate exception.

I am using the following environment:

  • Boost v1.49
  • GCC v4.71 with the -std=c++11 argument.
2

There are 2 best solutions below

0
On

Thanks to the user @sehe, I came up with the solution presented in this answer using standard stream operators and a Custom Validator of Boost.Program_options.

Ignoring the DRY issue (as in the source comments mentioned), the solution looks pretty clean. I also incorporated the common pitfall desribed in https://stackoverflow.com/a/5517755/891439.

Source Code using GCC v4.81 and Boost.Program_options v1.56:

#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <sstream>
#include <string>

#include "boost/any.hpp"
#include "boost/program_options.hpp"

// Imagine the source code in the following namespace block is placed in an
// external library.
// The mapping between enumerators and strings violates the DRY principle, but
// that issue can be easily fixed, e. g. by using one [Boost.Bimap][4].
namespace fw {
namespace example {

enum class Booleans : std::uint8_t {
  kFalse = 0,
  kTrue = 1
};

std::ostream& operator<<(std::ostream& os, Booleans const& kBoolean) {
  switch (kBoolean) {
    case Booleans::kFalse:
      os << "false";
      break;
    case Booleans::kTrue:
      os << "true";
      break;
    default:
      os.setstate(std::ios_base::failbit);
      break;
  }

  return os;
}

std::istream& operator>>(std::istream& is, Booleans& boolean) {
  std::string token;
  is >> token;

  if ("true" == token) {
    boolean = Booleans::kTrue;
  } else if ("false" == token) {
    boolean = Booleans::kFalse;
  } else {
    is.setstate(std::ios_base::failbit);
  }

  return is;
}

}  // namespace example
}  // namespace fw

// The following source code is application specific code.
namespace fw {
namespace example {

void validate(boost::any& value,
              std::vector<std::string> const& kValues,
              fw::example::Booleans*,
              int) {
  boost::program_options::validators::check_first_occurrence(value);
  std::string const& kStringValue{
      boost::program_options::validators::get_single_string(kValues)};

  std::istringstream iss{kStringValue};

  Booleans real_value;
  iss >> real_value;

  if (!iss) {
    throw boost::program_options::validation_error{
        boost::program_options::validation_error::invalid_option_value};
  }

  value = boost::any{real_value};
}

}  // namespace example
}  // namespace fw

int main(int argc, char** argv) {
  fw::example::Booleans boolean;

  boost::program_options::variables_map variables_map;
  boost::program_options::options_description options_description{
      "Allowed Options"};
  options_description.add_options()
      ("help,h", "Display the help message.")
      ("bool",
       boost::program_options::value<fw::example::Booleans>(
           &boolean)->required(),
       "Set the boolean value.");

  try {
    boost::program_options::store(
        boost::program_options::parse_command_line(argc,
                                                   argv,
                                                   options_description),
        variables_map);

    if (variables_map.count("help")) {
      std::cout << options_description << "\n";

      return 1;
    }

    boost::program_options::notify(variables_map);
  } catch (boost::program_options::error const &kException) {
    std::cerr << "Program Options Error: "
              << kException.what()
              << "\n\nSpecify -help for further information.\n";

    return 2;
  }

  if (variables_map.count("bool")) {
      std::cout << "The boolean value was set to "
                << variables_map["bool"].as<fw::example::Booleans>()
                << ".\n";
  }
}
  1. Can anyone provide further improvement suggestions?
  2. Any general comments regarding the source code?
1
On

The problem appears to be that you're (ab)using the streaming operator to do validation.

Instead, supply a Custom Validator to do the validation.

In that case you no longer have the conflict with the behaviour of operator<<.


The best you could hope for when adding a competing overload inside the program_options namespace would be that the compiler would find the overload acceptable /too/ and hence fail to compile it with "ambiguous overload"