Tuesday, October 28, 2008

C++ is fun again!

Lately at work I've been playing around with C++ and the Boost libraries; specifically I have been using Boost.Any, Boost.Filesystem, Boost.Program_ptions, Boost.Python, and last but not least Boost smart pointers.

These libraries, in conjunction with some of my own macros and setup have made for a very nice transition back to C++ from C#. The awesome things about C# are the class libraries and the syntactic sugar (foreach, lambdas, anonymous methods). Similar things can be done with C++ and Boost (Boost.Foreach, etc).

I'm going to talk about the way I've setup my command line options parsing, to give you an idea as to how I am using Boost.

First off, I have a static class called OptionsParser which looks like this:

class OptionsParser 
{
public:
  static bool isParsed(void);
  static void parseOptions(const std::vector<std::string>& options);
  static void registerOptions(const std::string& owner, const boost::program_options::options_description& options);
  static void printAllOptions(std::ostream& output);
  static const std::vector<std::string> getUnrecognizedOptions(void) const;

  template<class T>
  static T getValue(const std::string& option, T def)
  {
    T result = def;
    if(!isParsed())
      throw std::exception("You must call parseOptions before trying to retrieve values");

    if(_variables_map.count(option))
      result = _variables_map[option].as<T>();

    return result;
  }

  template<class T>
  static T getValue(const std::string& option)
  {
    T result;
    if(!isParsed())
      throw std::exception("You must call parseOptions before trying to retrieve values");

    if(_variables_map.count(option))
      result = _variables_map[option].as<T>();

    return result;
  }

private:
  OptionsParser();

  static boost::program_options::variables_map _variables_map;
  static bool _parsed;
  static std::vector<std::string> _unrecognized;
  static boost::program_options::options_description* _all_options;
};
The real meat of this class are the two methods registerOptions and parseOptions.

registerOptions is used from the constructor of some static class instances I have which setup the options when statics are initialized (before main).

The OptionsProvider class is what I use for this, along with some macros:

class OptionDefinition
{
public:
  OptionDefinition(const std::string& option_string, boost::program_options::value_semantic* value, const std::string& description);

  const std::string& getOptionString(void) const;
  const boost::program_options::value_semantic* getValueSemantic(void) const;
  const std::string& getDescription(void) const;
private:
  std::string _option_string;
  boost::program_options::value_semantic* _value_semantic;
  std::string _description;
};

class OptionsProvider
{
public:
  OptionsProvider(const std::string& owner, const std::string& description, OptionDefinition* options[]);
  virtual ~OptionsProvider(void);

  const std::string& getOwner(void) const;
  const std::string& getDescription(void) const;
  const boost::program_options::options_description& getOptions(void) const;

private:
  std::string _owner;
  std::string _description;
  boost::program_options::options_description _options;
};

#define BEGIN_OPTIONS_ARRAY(owner) static OptionDefinition* Options##owner[] = { 
#define DECLARE_OPTION(option_string, value_type, description) new OptionDefinition((option_string), \
new boost::program_options::typed_value<value_type>(NULL), \
(description)),
#define DECLARE_BOOL_OPTION(option_string, description)   new OptionDefinition((option_string), \
(new boost::program_options::typed_value<bool>(NULL))->default_value(0)->zero_tokens(), \
(description)),      

#define DECLARE_DEFAULT_OPTION(option_string, value_type, def_value, description) new OptionDefinition((option_string), \
(new boost::program_options::typed_value<value_type>(NULL))->default_value(def_value), \
(description)),
#define END_OPTIONS_ARRAY()  NULL };   // add terminator to the end of the list

#define DECLARE_OPTIONS_PROVIDER(owner, description) static OptionsProvider OptionsProvider##owner(#owner, description, Options##owner);

#define OPTIONS_PROVIDER(owner) OptionsProvider##owner
Now, here is an example usage of these macros:

BEGIN_OPTIONS_ARRAY(TestManager)
DECLARE_DEFAULT_OPTION("testid", unsigned long, 0L, "Step ID from automation")
DECLARE_DEFAULT_OPTION("timeout", int, -1, "Number of seconds before stopping a\ntest with a timeout result")
DECLARE_DEFAULT_OPTION("board-path", fs::path, fs::path("boards"), "Path to directory containing board libraries")
DECLARE_DEFAULT_OPTION("test-config", fs::path, fs::path(""), "Path to file containing the test configuration")
DECLARE_DEFAULT_OPTION("manager-path", fs::path, fs::path("managers"), "Path to directory container manager libraries")
DECLARE_DEFAULT_OPTION("output-dir", fs::path, fs::path("output"), "Path to output directory")
END_OPTIONS_ARRAY()

DECLARE_OPTIONS_PROVIDER(TestManager, "TestManager options")
This sets up the array and then creates a static OptionsProvider instance which will register the options with the OptionParser static class. Then I can just call OptionsParser::parseOptions(vector_created_from_argv) and it will parse all the options into the boost::program_options::variables_map.

If I have a plug-in class that is loaded later, I just do something like this after it's options have been registered:

OptionsParser::parseOptions(OptionsParser::getUnrecognizedOptions())
I could probably just have an overload of parseOptions with no parameter and have it automatically use the unrecognized options, but I haven't decided on that yet.

Then in my classes I can do stuff like this to get option values:

_testid = OptionsParser::getValue<int>("test-id");
to retrieve the value. If I used DECLARE_DEFAULT_OPTION, it automatically sets up the default value since the boost library sets that up. If I don't, then I'd probably call the getValue overload that takes a default value.

1 comment:

Eva said...

Awesome Post! Probably my all-time favorite.