tinkering with c++ — boost::program_options

The next task that I recommend new coders try after they learn how to write Hello World is how to pass arguments to an application on the command line and read those arguments in. Back in the day I’d write it all myself, because, well, there was about all there was. This time I’m using Boost’s Program Options library to parse and handle command line input. This is in the same vein to Python’s argparse, at least with regards to using another package.

#include <exception>#include <iostream>#include <boost/program_options.hpp>namespace po = boost::program_options;int main(int argc, char *argv[]) {po::options_description options("Options");options.add_options()("help", "Help using application.")("compression", po::value<int>(), "Set compression level to integer value");try {po::variables_map varmap;po::store(po::parse_command_line(argc, argv, options), varmap);po::notify(varmap);if (varmap.empty() or varmap.count("help")) {std::cout << options << std::endl;}else if (varmap.count("compression")) {std::cout << "Compression level set to "<< varmap["compression"].as<int>() << std::endl;}else {std::cout << "Compression level was not set.\n";return -1;}}catch (std::exception &ex) {std::cerr << "Error: " << ex.what() << std::endl;std::cout << std::endl << options << std::endl;return -1;}return 0;}

The example above started with an example provided on the Boost Program Options Getting Started page ( https://www.boost.org/doc/libs/1_80_0/doc/html/program_options/tutorial.html#id-1.3.30.4.3 ). The first code example in the tutorial won’t compile. In order to compile that code you need to include three libraries as shown in the first three lines of the listing above. I will make an editorial comment about the code examples in the Boost tutorial, and say that they’re all bad. Not only will they not compile, they won’t run very well either. I’ll get to that running poorly in a moment.

The bulk of the original tutorial logic is between lines 5 to 30. Everything else I had to add in order to produce an application that would behave reasonably, especially when erroneous input is given to the application.

The try/catch block was added in order to handle exceptions that the Boost library throws when it has an error. That isn’t documented anywhere that I can find for this library. While handling exceptions is really no big deal, and is considered the Python Way (using try/except) within Python, at least you should tell someone what will happen if your library reaches a point where an error occurs. I discovered this behavior when I successfully compiled the initial version without any sort of try/catch logic. Every time I gave it bad switches, or nothing at all, it would throw an exception and core dump. Yeah, that’s gonna work just fine. Not.

The rest of the code is the try/catch block, with some logic in the catch section to tell you what the error is and then prints out help. That’s what should happen.

If you just type the name of the application, line 20 will check to see if any switches were passed to the application. If none were, or if you entered --help, then you get help. The original code would not check for lack of any options and throw an exception and core dump. By the way, the library will only accept an option if it is preceded by --. Anything else, such as one dash, is treated as an invalid input.

The only problem with the listing is the last else statement, lines 27 to 30. With the logic as it currently exists, this last statement is no longer executed and should be removed. I left it in place because it’s part of the original Boost tutorial.

Setting up the Boost Build Environment

There are two ways to set up the Boost build environment. The first is to download the source tarball, unpack that into a work area, build it, then install it. The directions on the Boost website ( https://www.boost.org ) are clear and complete on this. I’ve done it myself just to see what it’s like and if it provides any advantage over installing from the Ubuntu repos. That depends. The current Boost release is version 1.80, released this November. The version installed from the repo is 1.74, released in August 2020, over two years ago. If you want the latest release with all its bug fixes and newer features, then yes, build Boost from the source and then build your application(s) on top of that. For the most part start with the repo and then build Boost from latest if you find a problem or you want a feature in version 1.80 that’s not in version 1.74.

The second way is to install from the repo. Specifically, sudo apt install libboost-all-dev will install all header and library files. The header files are installed in /usr/include/boost/, and the library files are installed in /usr/lib/x86_64-linux-gnu/.

Building the Boost Example

I used to be a make file nerd back in the day because make was all you had, and so you learned to use it. One of the primary reasons for make back in the day was because build systems were so slow compared to today’s systems. Make checked dependencies and only rebuilt those portions of a project that had changed, then rebuilt those changed source files and then relinked the full application binary. That’s not such a problem today, especially when my $300 2 GHz 8 core AMD Ryzen 5 blows the doors off those SUN Microsystems build servers I used to use. And Linux Mint is so much better than Solaris.

I’ve since fallen away from make, especially when all I want to do is build some simple applications. These days I’ll use Bash or Python, or if it gets complicated, CMake. If I write in Go or Rust, those two languages have a complete toolset that will build my application for me. For building this application I wrote a simple Bash script.

#!/bin/env bashg++ -std=c++20 -c cli.cppg++ -static -o cli cli.o -l boost_program_options

Line 1 is the standard shebang. Line two invokes the GNU C++ compiler to compile the application using the C++20 standard rules. I set the standard to C++20 because that’s what I’m brushing up on at the moment. If you’re running on a Linux distribution that has GNU C++ version 11 or later, then you’re all set. Linux Mint 21 has version 11.3.0. Line 3 is the linker line. Note the -static command line switch, and also note that I’ve had to select the specific library to link against with the -l boost_program_options command line switch. I’m building a static application because that is how Go and Rust build binaries. I heartily approve of static linking so that you can give your customers a single binary that will run just about anywhere. Having your application depend upon whether they have every supporting library installed is a royal pain. That may be the way things were done twenty or more years ago, but not today.

Running the Application

Here are a few runs of the application.

~ ./cli Options:  --helpHelp using application.  --compression arg Set compression level to integer value

Simply running the application without any input produces help. Just as you would want it to do.

~ ./cli --helpOptions:  --helpHelp using application.  --compression arg Set compression level to integer value

Running the application for explicit help.

~ ./cli --compressionError: the required argument for option '--compression' is missingOptions:  --helpHelp using application.  --compression arg Set compression level to integer value

We passed a proper option, but the argument is missing, thus the error. And once again we print out the help.

~ ./cli --compression 10Compression level set to 10

Now we have a successful invocation. But what happens if we try to pass a float argument? Remember, help says to pass an integer argument.

~ ./cli --compression 10.1Error: the argument ('10.1') for option '--compression' is invalidOptions:  --helpHelp using application.  --compression arg Set compression level to integer value

We get an error and another dose of help. What about a totally different option?

~ ./cli --fooError: unrecognised option '--foo'Options:  --helpHelp using application.  --compression arg Set compression level to integer value

Which is what we would expect. Before you ask, it will bale on on the first unrecognized option. And I know that “unrecognised” is misspelled in the error message. But that error message is part of the package, nothing that I can easily fix. There’s also one other feature. If I pass --comp or even --c, the code will recognize it because it’s still a partial match and it’s unique.

Some Final Thoughts

I choose to dig deeper into Boost because of the general good chatter around the library set. And the first few forays into Boost were very easy. Those first few forays used header-only libraries that didn’t require linking. This is the first instance where I had to link. To be honest, this experience has been a bit rough. If I’d had experience with this first, I might not have gone any farther. But it does show that quality in Boost is uneven. Not just in the tutorials, but in the implementation. For example consider the penchant to throw exceptions all over the place. Boost has two exception libraries, a regular Exception and another called LEAF. If you go to the Exception documentation they tell you that if you’re past a certain Boost version then you should use LEAF. I looked at LEAF and left in confusion. I tried Boost’s Exception and discovered that Boost’s Exception printed out that it was using regular C++ exception and that maybe the application should use the more advance Boost Exception. Really?

I’m going to continue to survey Boost as well as move on my own into C++ 20 style C++.

c++ error messages

Example C++ code with error messages

I’ve been working a bit with GNU C/C++ (version 11.3) on Linux Mint 21. I’ve been trying to use Visual Studio Code for coding, thinking that it, along with Microsoft’s extension, would help me write more effectively by pointing out problems/errors and providing suggested solutions. Sometimes that works, sometimes it doesn’t, meaning that I get the error but no idea how to solve it, no clues whatsoever. Rather, if I take the problematic code and let GNU C++ try to compile it, I get very verbose errors that include helpful suggestions on how to correct the problem.

This post uses a very simple program that requires C++ in order to compile and execute. Here’s the correct listing.

#include <iostream>#include <iterator>int main(int argc, char *argv[]) {std::copy(argv, argv + argc, std::ostream_iterator<char *>(std::cout, "\n"));}

It’s just six lines of code (five if you ignore the whitespace). The key line is line 5, a one-liner that prints out all the arguments passed along to the application when it’s called, one argument/line. I didn’t create this, but I learned it decades ago and have kept it in my “back pocket” along with many other bits of proven and useful code.

The problem occurred in another application when I failed to include a library that required an iterator definition. I eventually fixed it, but only on the command line using GNU’s C++. I then whipped up this example to show what I’m complaining about. If you look at the top screen capture you’ll see that VS Code has put red squiggles underneath the problem code. I’ve deliberately used an ostream_iterator that calls out for the inclusion of the iterator library. Unfortunately, as you’ll note above, clicking on the bad code in VS Code does nothing more than state the obvious. No clue how you might fix this. And before you say anything, I’ve worked with other IDEs in the past that actually did offer suggestions, and even offered to fix it for you. For example, Android Studio and the IDE it was derived from, JetBrains’ IntelliJ IDE. Granted those tools are all about Java and Kotlin, but still, they’re a lot more helpful and productive.

In the end it was GNU’s C++ that provided all I needed to fix the contrived problem.

cli.cpp: In function ‘int main(int, char**)’:cli.cpp:5:39: error: ‘ostream_iterator’ is not a member of ‘std’5 | std::copy(argv, argv + argc, std::ostream_iterator<char *>(std::cout, "\n"));  |   ^~~~~~~~~~~~~~~~cli.cpp:2:1: note: ‘std::ostream_iterator’ is defined in header ‘<iterator>’; did you forget to ‘#include <iterator>’?1 | #include <iostream>  +++ |+#include <iterator>2 | //#include <iterator>

This help included not only what library definition to include, but where (yes, I commented out the original to break it). Line five in the error listing shows you where ostream_iterator is defined, and line 7 helpfully shows you where to place the include in your code. This kind of compiler help reminds me of the help that Rust’s compiler also produces when it comes across a problem.

My experience with C++ goes back to 1986 when it was first called C with Classes and you had to run cfront on your C++ code to produce C code that was then compiled and linked. All this on a Micro VAX running Ultrix, DEC’s version of BSD Unix (which was nothing more than bog-standard BSD with all the licensing text replaced with DEC). I know it’s so much fun denigrating C++, but it’s evolving to answer the critiques and the needs of its users, both in the language as well as the tooling.