CLI in C++: Existing Solutions

This is the fourth installment in the series of posts about designing a Command Line Interface (CLI) parser for C++. The previous posts were:

In the last post we analyzed various ways to represent the options information in the application as well as established a set of properties that the ideal solution would have. Today we examine two existing implementations, the Program Options library from Boost as well as the CLI library from libcult, and determine how close these solutions approach the ideal.

The Boost Program Options library provides three ways to represent the options information: as a set of variables defined by the user (except for options without values which we call flags), as a heterogeneous map of option names to values, and by calling and passing the value to a user-provided callback function. All three methods can be used simultaneously for different options. In the previous post we have analyzed the first two approaches to storing the options information. The third approach seems rather cumbersome since it makes the user go an extra mile to get the option value. I can’t think of any scenario where it would be much more convenient than the other two. Here is an example of using the first approach with Program Options:

unsigned short compression;
 
int main (int argc, char* argv[])
{
  po::options_description desc;
  desc.add_options ()
    ("compression",
     po::value<unsigned short>(&compression)->
       default_value (5),
     "compression level");
 
  po::variables_map vm;
  po::store (
    po::parse_command_line (argc, argv, desc), vm);
  po::notify (vm);
}

As we have noted before, this approach does not scale well to the modular design of more complex applications and suffers from the verbosity problem (the option name is repeated three times, the option type — twice).

And here is an example that instead uses the heterogeneous map to store option values:

int main (int argc, char* argv[])
{
  po::options_description desc;
  desc.add_options ()
    ("help", "show usage information")
    ("version", "show version")
    ("compression",
     po::value<unsigned short>()->default_value (5),
     "compression level");
 
  po::variables_map vm;
  po::store (
    po::parse_command_line (argc, argv, desc), vm);
  po::notify (vm);
 
  if (vm.count ("help"))
  {
    ...
  }
 
  compressor c (
    vm["compression"].as<unsigned short> ());
}

Again, as we have discussed before, this approach has a number of drawbacks including the use of strings to identify options and the need to specify the option type every time we retrieve its value. And we also have the verbosity problem as in the previous approach.

Overall, the use of operator() in Program Options to collect option descriptions makes the code feel foreign to the conventional ways of doing things in C++. Every time I look at it I need to make an effort to understand what’s going on there. The need to make three function calls just to parse the simplest command line feels arbitrary.

The next implementation to consider is the CLI library from libcult which was my previous attempt at designing a statically-named and typed options representation. Here is how we would handle the above example using this library:

extern const char help[] = "help";
extern const char version[] = "version";
extern const char compression[] = "compression";
 
typedef
cli::options<help, bool,
             version, bool,
             compression, unsigned short>
options;
 
typedef cli::options_spec<options> options_spec;
 
int main (int argc, char* argv[])
{
  options_spec spec;
  spec.option<compression> ().default_value (5);
  options o (cli::parse (spec, argc, argv));
 
  if (o.value<help> ())
  {
    ...
  }
 
  compressor c (o.value<compression> ());
}

The option accessors are statically-typed. The option names used as the template arguments are C++ variables so any misspelling is detected by the compiler. There is still the verbosity problem with three repetitions of the name for each option.

The more important problem with this approach, however, is in the implementation details. Templates and template specializations are used heavily to make this interface possible. With a handful of options this is not a problem. However, for applications with hundreds of options this becomes taxing in terms of the compilation time and object code size. The code size issue stems from the long symbol names caused by template instantiations with hundreds of template arguments.

To summarize, the Program Option library falls far short from the ideal we have established. The CLI library from libcult has most of the properties of the ideal solution but does not scale to the large number of options.

Next time we will start exploring the possible ways of implementing our ideal solution. If you have any thoughts, feel free to add them as comments.

Comments are closed.