In this tip I want to show a quick way (one of many) for parsing command line arguments using C++17.

A quick way of parsing command line arguments in modern C++-17 for non-production without using third party header files / libraries. For real world projects, a third party approach is more encouraged.

Background

Very often, by learning new libraries or creating small playground apps (for example, which fit in a single file or a couple of files), I need to parse command line arguments, but don’t want mental/time overhead of importing whole libraries (like boost) or lookup third party header files just for that purpose. The work sequence in that scenario is something like this:

  1. Fire VIM, VS, XCode.
  2. Try out some code.
  3. Compile and run with passing some test command line arguments.
  4. If everything clear/OK, goto 2 otherwise goto 5.
  5. Do something else.

Using the Code

As always, when designing some feature or library, I usually go from the use-case first, and then look for appropriate implementation.

The use-case for parsing command line arguments, which I found most convenient for myself, looks something like this:

int main(int argc, const char* argv[])
{
    struct MyOpts
    {
        string stringOpt{};
        int intOpt{};
        bool boolOpt{};
    };

    auto parser = CmdOpts<MyOpts>::Create({
        {"--opt1", &MyOpts::stringOpt },
        {"--opt2", &MyOpts::intOpt},
        {"--opt3", &MyOpts::boolOpt}});

    auto myopts = parser->parse(argc, argv);

    std::cout << "stringOpt = " << myopts.stringOpt << endl;
    std::cout << "intOpt = " << myopts.intOpt << endl;
    std::cout << "boolOpt = " << myopts.boolOpt << endl;
}

The MyOpts struct is holding the options which I intend to pass via command line. So in this case, we will have one string option, one int option and one bool option:

struct MyOpts
{
    string stringOpt{};  // will be holding some string option from command line
    int intOpt{};        // will be holding some int option from command line
    bool boolOpt{};      // will be holding some boolean option from command line
};

Now, those options should have some names. In this case, I want to name my string option opt1, the int option should be named opt2, and the boolean option should be named opt3. The command line in this case would then look like this:

./main.exe --opt1 some-string-option --opt2 33 --opt3 1

So, the “some-string-option” should be assigned to MyOpts::stringOpt33 should be assigned to MyOpts::intOpt and true should be assigned to MyOpts::boolOpt.

To achieve this, I came up with the following implementation for CmdOpts:

template <class Opts>
struct CmdOpts : Opts
{
    using MyProp = std::variant<string Opts::*, int Opts::*, double Opts::*, bool Opts::*>;
    using MyArg = std::pair<string, MyProp>;

    ~CmdOpts() = default;

    Opts parse(int argc, const char* argv[])
    {
        vector<string_view> vargv(argv, argv+argc);
        for (int idx = 0; idx < argc; ++idx)
            for (auto& cbk : callbacks)
                cbk.second(idx, vargv);

        // yes, we are slicing ... Opts should be POD
        // -> we could also enforce it via type traits
        return static_cast<Opts>(*this);
    }

    static unique_ptr<CmdOpts> Create(std::initializer_list<MyArg> args)
    {
        auto cmdOpts = unique_ptr<CmdOpts>(new CmdOpts());
        for (auto arg : args) cmdOpts->register_callback(arg);
        return cmdOpts;
    }

private:
    using callback_t = function<void(int, const vector<string_view>&)>;
    map<string, callback_t> callbacks;

    CmdOpts() = default;
    CmdOpts(const CmdOpts&) = delete;
    CmdOpts(CmdOpts&&) = delete;
    CmdOpts& operator=(const CmdOpts&) = delete;
    CmdOpts& operator=(CmdOpts&&) = delete;

    auto register_callback(string name, MyProp prop)
    {
        callbacks[name] = [this, name, prop](int idx, const vector<string_view>& argv)
        {
            if (argv[idx] == name)
            {
                visit(
                    [this, idx, &argv](auto&& arg)
                    {
                        if (idx < argv.size() - 1)
                        {
                            stringstream value;
                            value << argv[idx+1];
                            value >> this->*arg;
                        }
                    },
                    prop);
            }
        };
    };

    auto register_callback(MyArg p) { return register_callback(p.first, p.second); }
};

Now let’s break this apart:

CmdOpts is a class template. Its template parameter is type of an object which command line arguments will be assigned to. In the example above, it would be MyOpts.

* The MyProp type alias:

using MyProp = std::variant<string Opts::*, int Opts::*, double Opts::*, bool Opts::*>;

s conceptually a union (std::variant is a C++17 type safe union), where each template parameter is of type  pointer to member of Opts. So, at every given timepoint, a variable of type MyProp will hold a pointer to exactly one member of Opts (in our example MyOpts). At this point, we are restricting us to 4 different types: stringintdouble and bool. So we are basically saying: every command line argument we pass, will be converted to one of those 4 types. This isn’t ideal and could certainly be improved, but remember: we want to keep our implementation minimalistic and simple. And for command line argument, passing those four types would normally be sufficient.

MyArg will be holding a mapping between option names (for example --opt1) and members of our Opts object (for example, stringOpt), where the values from the command line will be assigned to.

* For every option we will define a callback, so when the option is encountered on the command line, the callback will execute and assign the value to the associated object. So for this, we are holding a mapping between option names and callbacks:

using callback_t= function<void(int, const vector<string_view>&)>;
map<string, callback_t> callbacks;

The callback is a function type, whose parameters are the current index of the command line arguments being passed and a view to all available command line arguments. This is needed to get the argument value:

//    argv[idx]   argv[idx+1]
... --optionName optionValue ....

CmdOpts::parse is the main function for our parser. It basically goes over the entire command line arguments and execute each callback in turn. The algorithmic complexity O(n2) is not an issue here, unless we expect to have a big number of command line args, which would be bad use anyway (input file would be more appropriate in this case).

* CmdOpts::register_callback(string name, MyProp prop)

CmdOpts::register_callback sets the callback for the option named name. If the option with this name is encountered, the value is read and written to object property propNote: we are using stringstream and variant visitor here, for doing the conversion for different types automatically: every type comfortable with standard input operator >> is ok. Note also that boolean input is expected 0 and 1 (instead of false/true).

And that’s it! Here’s the complete source file for the above example:

#include <functional>   // std::function
#include <iostream>     // std::cout, std::endl
#include <map>          // std::map
#include <memory>       // std::unique_ptr
#include <string>       // std::string
#include <sstream>      // std::stringstream
#include <string_view>  // std::string_view
#include <variant>      // std::variant
#include <vector>       // std::vector
using namespace std;

template <class Opts>
struct CmdOpts : Opts
{
    using MyProp = std::variant<string Opts::*, int Opts::*, double Opts::*, bool Opts::*>;
    using MyArg = std::pair<string, MyProp>;

    ~CmdOpts() = default;

    Opts parse(int argc, const char* argv[])
    {
        vector<string_view> vargv(argv, argv+argc);
        for (int idx = 0; idx < argc; ++idx)
            for (auto& cbk : callbacks)
                cbk.second(idx, vargv);

        return static_cast<Opts>(*this);
    }

    static unique_ptr<CmdOpts> Create(std::initializer_list<MyArg> args)
    {
        auto cmdOpts = unique_ptr<CmdOpts>(new CmdOpts());
        for (auto arg : args) cmdOpts->register_callback(arg);
        return cmdOpts;
    }

private:
    using callback_t = function<void(int, const vector<string_view>&)>;
    map<string, callback_t> callbacks;

    CmdOpts() = default;
    CmdOpts(const CmdOpts&) = delete;
    CmdOpts(CmdOpts&&) = delete;
    CmdOpts& operator=(const CmdOpts&) = delete;
    CmdOpts& operator=(CmdOpts&&) = delete;

    auto register_callback(string name, MyProp prop)
    {
        callbacks[name] = [this, name, prop](int idx, const vector<string_view>& argv)
        {
            if (argv[idx] == name)
            {
                visit(
                    [this, idx, &argv](auto&& arg)
                    {
                        if (idx < argv.size() - 1)
                        {
                            stringstream value;
                            value << argv[idx+1];
                            value >> this->*arg;
                        }
                    },
                    prop);
            }
        };
    };

    auto register_callback(MyArg p) { return register_callback(p.first, p.second); }
};

int main(int argc, const char* argv[])
{
    struct MyOpts
    {
        string stringOpt{};
        int intOpt{};
        bool boolOpt{};
    };

    auto parser = CmdOpts<MyOpts>::Create({
        {"--opt1", &MyOpts::stringOpt },
        {"--opt2", &MyOpts::intOpt},
        {"--opt3", &MyOpts::boolOpt}});

    auto myopts = parser->parse(argc, argv);

    std::cout << "stringOpt = " << myopts.stringOpt << endl;
    std::cout << "intOpt = " << myopts.intOpt << endl;
    std::cout << "boolOpt = " << myopts.boolOpt << endl;
}

Leave a Reply

Your email address will not be published. Required fields are marked *