Real-Time Systems Inc.

Specializing in Embedded Controller Design


A Table-Driven Command Processor in C++

Many embedded controllers can benefit from a command-line interface, even if only during development. A serial port and a bit of software is all that's needed to provide easy access to the internals of the system at run-time. With proper design, the same software can be used unchanged via TCP/IP, USB, or any other byte-stream link.

To make the system easy to use, (and easy to drive with scripts), it should present a uniform user interface -- consistent prompting, parsing, and error display. Even more than most subsystems, a command processor benefits from a library-based approach.

This article presents a light-weight command processor in C++ in two parts: the basic engine and some optional enhancements. It demonstrates real-world use of a number of C++ techniques: iostreams, templates, member-function pointers, custom type conversions, new-style casts, and placement new. These techniques are used not just as examples but to solve real problems and to make real improvements in the code.

What we'd like to support

Let's start with an example of the desired client code then build an engine to support it. Rather than centralize all command-line parsing, let's distribute the parsing logic to the places where the information itself exists. In that way, we'll encourage encapsulation of the information and keep modifications and upgrades more localized.

As an example, consider setting the baud rate of a serial port driver from the system console at run-time. We'd like to type a command such as:

    > baud 57600

and have the baud-rate change. (For the sake of simplicity, let's assume we're changing the rate on some port other than one we're typing the command on.) Let's also support displaying the current baud rate by giving the command with no arguments:

    > baud
      current baud rate: 57600
    >

To maintain good encapsulation in the serial port driver, the current baud rate should be a private member, accessible only to methods of the driver class:

    class SerialPort
    {
      unsigned baudRate;

    public:
      SerialPort() : baudRate(9600) { }

      // ...
    };

The "baud" command will need to somehow cause a call to a SerialPort member function; we'll discuss below how this can be acheived. For now, let's just consider how to parse the command-line arguments (the command "tail").

Since this is C++, let's use the iostream library for parsing input and formatting responses. We'll call the baud command handler with an istream containing the command tail (the rest of the input line after the command name "baud"). We'll also pass in two ostream's to respond on: one for normal output and another for reporting parse errors.

    class SerialPort
    {
      // as above

      void baudCmd(istream& in, ostream& out, ostream& err)
      {
        if (/* no more text on input line */)
          out << "current baud rate: " << baudRate << endl;
        else
        {
          unsigned b;
          if (in >> b && 300 <= b && b <= 115200)
          {
            baudRate = b;
            // copy baudRate to hardware
          }
          else
            err << "need baud rate between 300 and 115200" << endl;
        }
      }

      // rest of the driver...
    };

We first look for the absence of further text on the input line after the command (see below for one way to do this). If we find no trailing text, we show the current setting on the normal output stream, out.

We then attempt to parse an unsigned number from the input stream with "in >> b". This expression calls the overloaded operator:

    istream& operator >> (istream&, unsigned&)

which skips any leading whitespace then looks for a sequence of ASCII digits in the stream it can interpret as an unsigned number. If it's successful, operator >> writes the result to b. The operator returns an istream reference; we'll use that to find out whether the conversion succeeded.

The && operator has an istream& on its left-hand side, but of course needs a boolean. The istream class provides a conversion operator for just this sort of circumstance; it effectively returns a bool: true if there have been no parse errors on the stream, and false otherwise.

So if parsing the baud rate succeeded, we check that the number is within a valid range. If so, we set the baud rate; otherwise, we show an error message on the error stream, err.

This example shows the pattern most command handlers will use: get arguments (if any) from in, write results to out, and complain if necessary on err.

The command buffer

Since embedded controllers are often autonomous, running for long periods without an operator present, they have higher reliability requirements than other systems. One way to improve reliability is with consistency and predictability in interfaces. Simplicity also helps.

We're building a light-weight embedded command processor, so we don't really need things like multi-line commands and fancy escaping schemes. We'll therefore assume simple line-oriented input. In other words, each command will be confined to a single input line. Also, to ensure absolute consistency when using various sources of characters (serial ports, Telnet sessions, or whatever), we won't expect the source to perform line editing; we'll instead collect raw characters from the source and do our own line editing.

Hooking up the streams

The istream seen by the command handlers will thus be connected not to the actual source of characters, but instead to a line buffer filled from the real source by our line editor. Filling the line buffer at this level also ensures there are no buffer overrun errors from overly long command lines. We can also ignore all but the essential control characters, thereby filtering out all manner of invalid input before feeding it to the command parser.

Since we'll be writing lots of those little command handlers, we'll try to make them easy to write. To that end, let's collect the three streams into a single class with the line buffer:

    class CmdBuf
    {
      istream& is;      // actual source of commands
      ostream& os;      // output stream for results and error messages

      char line[80];        // input line buffer
      istrstream iss;       // stream for access to line buffer

      // more later

    public:
      istream& in() { return iss; } // get access to input line
      ostream& out(); { return os; }    // connect to output stream
      ostream& err(); iss.set(ios::failbit);  return os; }
                    // note failure and use output stream

      bool fill();      // fill line buffer from input stream

      // more later
    };

We use an istrstream (which is derived from istream) to give istream-like access to the input line buffer after it's been filled from the real input stream, is. To the client, the istrstream looks and works just like an istream, except that the end of the input line appears as end-of-file on the stream. We can use this feature to our advantage when testing for end-of-line.

The baud-rate handler now takes a single CmdBuf argument, from which it extracts the streams it needs by calling methods on the CmdBuf:

    void SerialPort::baudCmd(CmdBuf& buf)
    {
      if ((buf.in() >> ws).eof())
        buf.out() << "current baud rate: " << baudRate << endl;
      else
      {
        unsigned b;
        if (buf.in() >> b && 300 <= b && b <= 115200)
        {
          baudRate = b;
          // copy baudRate to hardware
        }
        else
          buf.err() << "need baud rate between 300 and 115200" << endl;
      }
    }

We can now easily detect an empty command tail by simply stripping whitespace and testing for end-of-file. The expression (buf.in() >> ws) pulls characters from the stream until it finds a non-whitespace character, returning an istream&. The ".eof()" checks if that non-whitespace character is the end-of-file marker, which for an istrstream indicates the terminating zero at the end of its character buffer.

If an error occurs during parsing, the handler can complain via the stream returned by CmdBuf::err(). As well as with providing an output stream, err() records the failure and halts any further parsing of the input stream by marking it "failed".

Constructing the CmdBuf

Before collecting a line from the user (see below), CmdBuf::fill() prompts the user. The prompt string is passed in to the CmdBuf constructor, along with the input and output streams:

    class CmdBuf
    {
      // as before

      const char* prompt;       // prompt string

    public:
      CmdBuf(istream& i, ostream& o, const char* p)
      : is(i), os(o), prompt(p), iss(line, 1)
      {
        strcpy(line, " ");
      }

      // as before
    };

Initializing the istrstream is a bit awkward since specifying a buffer length of zero has a special meaning for istrstream's: assume a zero-terminated string and measure its length. Since we can't zero-terminate the line buffer before the constructing the istrstream (the line buffer is an array, which can't be initialized in the way the other members can), we need to specify a non-zero length then initialize the buffer contents to match with strcpy().

Filling the buffer

CmdBuf::fill() fills the line buffer from the input stream, offering simple line editing. It returns true if there's a command line to parse or false if the input stream hits end-of-file.

First show the prompt:

    bool CmdBuf::fill()
    {
      os << prompt << flush;

We want the user to see the prompt immediately, regardless of any buffering in the ostream; the flush manipulator ensures this.

Next, set up pointers into the line buffer:

      char* bp = line;                      // current character
      char* ep = &line[sizeof(line)];       // end of input area

We'll loop until we see a newline, carriage return, or EOF. Newline and carriage return will set a boolean to cause the loop to exit; EOF will cause an immediate return from fill().

As we receive characters, we'll echo them (mostly), but we don't need to flush the output stream until we're forced to wait for input from the user. (Flushing can be expensive on some streams, so we avoid it until necessary.) Use is.rdbuf()->in_avail() to obtain the number of input characters which can be retrieved without waiting; after the optional flush, get one character from the input stream and parse it.

      bool done = false;
      while (!done)
      {
        if (is.rdbuf()->in_avail() == 0)    // no characters available?
          os.flush();

        int c = is.get();   // get a character; wait if necessary
        switch (c)
        {

Newline or carriage return indicate the user is finished with the command zand wants it executed, so move the cursor to a new line and force an exit from the loop. Mark this case with the symbol exit since we'll need this "done-with-command" sequence in other cases below.

~ case '': case '': // end input exit: os << endl; done = true; break; ~ {.cpp}

If we find EOF on the input stream, there may have been text before it on the input line which should be treated as the final command. If so, perform the normal end-of-line sequence. When fill() is run again, the EOF will appear at the beginning of the line, so we can then return false to indicate it. zz ~ {.cpp} case EOF: if (bp != line) // last command, ends at EOF goto exit; return false; // true EOF, report it ~

It can be annoying to retype lines to repeat a command or fix typing errors. With just a few lines of code, we can easily re-use the contents of the line buffer to allow repeating or editing the previous command. To repeat a command, display the contents of the line buffer and exit the loop, just as for newline. To edit the previous command, display the line buffer and stay in the loop to wait for another character from the user.

         #define ctl(c) ((c) - '@')

          case ctl('R'):        // repeat command
            while (*bp)
              os << *bp++;
            goto exit;

          case ctl('E'):        // edit previous command
            while (*bp)     
              os << *bp++;
            break;

For backspace or delete characters, back up the buffer pointer and the console cursor by one and erase the character under the cursor. This implements the traditional single-character backspace function on a video terminal.

If we repeat this backspace sequence to the beginning of the line, we have a simple line-erase function.

          case ctl('H'):  case 0x7F:        // backspace/delete
            if (bp > line)
            {
              --bp;
              os << "\b \b";
            }
            break;
            
          case ctl('X'):                    // line erase
            while (bp > line)
            {
              --bp;
              os << "\b \b";
            }
            break;

Any other character gets echoed and goes in the buffer, as long as it's printable and there's still room. Dropping non-printable characters avoids all sorts of parsing confusion and problems with invisible control characters messing up console displays.

          default:                          // all other characters
            if (isprint(c) && bp < ep)
              os << (*bp++ = c);
            break;
        }

Zero-terminate the string in the line buffer after each character is handled. Doing so ensures that regardless of what causes an exit from the loop, we always have a valid string and can replay when repeating or editing the previous command, and that we can use strlen() to determine the command length later to set up the istrstream.

        *bp = 0;                            // terminate after each change
      }      

We've exited the collection loop, so the line buffer now holds a complete command. We now need to reinitialize the istrstream to provide access to it. operator new() usually gets memory from the heap before running a constructor on it, but a special variant called "placement new" allows us to specify the address of the object to construct. In our case, we want to (re-)construct iss, the istrstream, giving the buffer start and command length as arguments:

      new (&iss) istrstream(line, strlen(line));
      return true;
    }

CmdBuf::fill() returns true to indicate the stream has not reached end-of-file.

Selecting commands

Before our digression into the command-line editor, we showed how a command handler can parse a command's arguments; we now need to parse the command name and run the command handler itself.

Command lookup

We could use an if-then chain to parse the command name, but this quickly becomes tedious:

    CmdBuf buf(is, os, "> ");

    SerialPort serialDriver(...);
    MotorDriver motorDriver(...);
    SystemVersion systemVersion(...);

    char token[20];
    if (buf.in() >> setw(sizeof(token)) >> token)
    {
      if (strcmp(token, "baud") == 0)
        serialDriver.baud(buf);

      else if (strcmp(token, "speed") = 0)
        motorDriver.speed(buf);

      else if (strcmp(token, "version") = 0)
        systemVersion.show(buf);

      else...

A table-driven approach will be simpler and more concise and will allow passing command menus as arguments, if desired.

Each entry in a command table will need certain fields:

The help string is not strictly necessary, but it allows us to list out the commands with a short explanatory phrase for each one.

Structure of the table

We can structure the table in many ways: an array of pointers to entries, a linked list of entries, or a simple array of entries. Any of these will work, but we want something quick to write, memory efficient, and if possible, not requiring heap storage. (Some embedded systems can't use heap storage, either due to memory shortages or for reliability reasons).

Using an array of pointers to entries probably means the entries themselves will be on the heap -- let's avoid that. A linked list could have entries on either the stack or the heap, but if they're on the stack, the storage for the "link" can be avoided by just putting the entries in a simple array.

Modern C++ thinking discourages arrays, but they still have their uses, especially when code and data memory are tight, and when we're trying to avoid dynamic memory.

A command table entry for storage in an array might be derived from a base class like the following. (Using a base class for the table entries will allow us to make other types of tables beside command tables):

  struct CmdBase
  {
    const char* const name; // command name
    const char* const info; // help string

    CmdBase(const char* n, const char* i) : name(n), info(i) { }
  };

Note that we've made everything public by declaring CmdBase a struct; this simplifies the search routine, below. Since both fields are const, there's no danger of them being corrupted; the only problem is that if we wish to change their definitions, we may have to change other code to match.

Command table entries using virtual functions

Now we need a way to specify the object to operate on and the method to call on it. If we require that the object be a class object, the method to call can be specified in one of two ways:

First, we could make the handler method a virtual function on a base class, then derive each client class from this base. For example, CmdFunc would become:

  class CmdFunc : public CmdBase
  {
    virtual void parse(CmdBuf&);    // command handler

    // ...
  };

Then SerialPort would be derived from CmdFunc, with the baudCmd() method renamed to parse():

  class SerialPort : public CmdFunc
  {
    unsigned baudRate;

    virtual void parse(CmdBuf& buf) // set the baud rate
    {
      if ((buf.in() >> ws).eof())
        buf.out() << "current baud rate: " << baudRate << endl;
      else
      {
        unsigned b;
        if (buf.in() >> b && 300 <= b && b <= 115200)
        {
          baudRate = b;
          // copy baudRate to hardware
        }
        else
          buf.err() << "need baud rate between 300 and 115200" << endl;
      }
    }
  };

This works fine until we want two or more parsers in one client class. For example, if SerialPort also needs handlers for parity, the number of data bits, and the number of stop bits, we're in trouble since we're not allowed to have multiple instances of one base class:

  class SerialPort, public CmdFunc, public CmdFunc, public CmdFunc
  {
    // Illegal!
  };

Command table entries using member-function pointers

Instead of virtual functions, we can store object pointers and member-function pointers in the CmdFunc objects. Unlike regular function pointers, member-function pointers are special objects which hold addresses of class member functions. (They're different from regular function pointers since they're capable of pointing to either normal or virtual member functions.)

Using member-function pointers, our command table could look something like this:

    SerialPort serialDriver(...);
    MotorDriver motorDriver(...);
    SystemVersion systemVersion(...);

    CmdFunc cmds[] = 
    {
      CmdFunc("baud", "    -- set baud rate", serialDriver, &SerialPort::baud),
      CmdFunc("parity", "  -- set parity", serialDriver, &SerialPort::parity),
      CmdFunc("data", "    -- set data bits", serialDriver, &SerialPort::dataBits),
      CmdFunc("stop", "    -- set stop bits", serialDriver, &SerialPort::stopBits),
      CmdFunc("speed", "   -- set motor speed", motorDriver, &MotorDriver::speed),
      CmdFunc("version", " -- show version", systemVersion, &SystemVersion::show),
      CmdFunc(0, "unknown command")
    };

The table is a simple array of anonymous CmdFunc objects, each constructed in-place while initializing the array. (By "anonymous" we mean that the CmdFunc objects do not have individual names.) As you can see by the constructor arguments, each CmdFunc holds a command name, a help string, a reference to an object, and a pointer to a method on that object's class.

But what is the data type of a CmdFunc's object? It appears to be potentially different for each CmdFunc object: SerialPort, MotorDriver, SystemVersion, and so forth. We could use void*'s to refer to a CmdFunc's object since they can point to any type of object. Unfortunately, C++ has no similar void-type pointer that can point to any member function. We'll need something different.

Whatever we do, we'd like the compiler to ensure that the class of the object and the class of the member-function pointer match. Errors like the following should be caught at compile-time:

    CmdFunc cmds[] = 
    {
      // ...

      CmdFunc("baud", "set baud rate", serialDriver, &MotorDriver::speed),

      //                                    ^-- mismatch! --^
      // ...
    };

C++ templates can do this sort of matching. We can't make CmdFunc itself a template, however, since in an array, all of the items must be of the same type, and a CmdFunc<SerialPort> is a different type than CmdFunc<MotorDriver>. We can, however, make the constructor of CmdFunc a template, then cast its arguments to a placeholder type inside the CmdFunc.

Let's call the placeholder type for the object "Object", and the placeholder for the parser method "Parser". The declarations will then look like this:

    class CmdFunc : public CmdBase
    {
      class Object;
      Object& object;

      typedef void (Object::*Parser)(CmdBuf&) const;
      Parser parser;

      // continued...

Object is a dummy class that we'll never create instances of; we'll only cast object references to Object references.

For the parser pointers we use a typedef to simplify the declarations. You can read the declaration as: "A Parser is a pointer to a method on class Object which takes a CmdBuf reference argument and returns nothing (void)". The "Parser parser;" declaration reads the same, after first substituting the actual pointer named parser for the typedef name Parser.

The main CmdFunc constructor is a template function; some of its arguments' types are determined by the actual objects passed in:

    class CmdFunc
    {
      // ...

    public:
      template <typename T>
      CmdFunc(const char* n, const char* i, T& t, void (T::*p)(CmdBuf&))
      : CmdBase(n, i),
        object(reinterpret_cast<Object&>(t)),
        parser(reinterpret_cast<Parser>(p))   
      { }

      // ...
    };

Wow. We'd better go through this slowly.

The first two constructor arguments n and i are straightforward -- they're the name and help strings, and they're simply passed along to the base class constructor.

"template <typename T> says that in this constructor, T will be replaced by an actual class type. Unlike in a class template where the type must be specified explicitly between angle brackets ("Foo"), in a function template like this the compiler can infer the template type from the type of the arguments. So in our example:

     // ...

     CmdFunc("baud", "set baud rate", serialDriver, &SerialPort::baud),

     // ...

the compiler sets T to SerialPort because that's the type of the third argument. This third argument, T& t, we cast to an Object& with reinterpret_cast<> and store in the object field. (reinterpret_cast<> completely ignores the type of its argument, forcing it to the output type. This, of course, makes us as programmers completely responsible for correctness, as we've thrown away type checking and the compiler can no longer help us.)

The fourth argument is the parser method pointer. The declaration uses the "::*" syntax to specify that p is a pointer to a method on T which takes a CmdBuf& and returns nothing. Note the similarity of this declaration to that of Parser, except for the class type. We use reinterpret_cast<> to force p to considered a Parser, then store it in the parser field.

These casts are safe since all object pointers are the same size and are manipulated in the same way when they're later dereferenced, regardless of the type of the object they actually point to. Likewise, all member-function pointers have the same form and are dereferenced in the same way, regardless of the actual class and member function they refer to. Furthermore, the compiler will require the types of t and p to be compatibile since they both involve the template argument T, so we won't be able to accidentally specify a mismatching object and parser method.

That's how we build the table entries, but we still need a way to terminate the table. We could simply measure the table length like this:

    unsigned cmdCount = sizeof(cmds) / sizeof(cmds[0]);

but that would mean passing both the table and its length to the search routine (below). Alternatively, we can end each table with a special terminator entry, for example, an entry with a null name pointer.

Since the table terminator doesn't need an object and parser method, we can use a separate constructor for this case. Even though we won't specify the object and parser, we'll find it advantageous later that the pointers be valid. For these special entries, let's just use the CmdFunc itself as the object and provide a dummy, do-nothing parser:

    class CmdFunc
    {
      // ...

      void null(CmdBuf&) { }    // dummy parser

    public:
      CmdFunc(const char* n, const char* i)
      : CmdBase(n, i),
        object(reinterpret_cast<Object&>(*this)),
        parser(reinterpret_cast<Parser>(&CmdFunc::null))   
      { }

      // ...
    };

For convenience, we can use the help message in list terminator entries as the error message to display when a token can't be found in the table. See above in the definition of cmds for an example.

We can also use this constructor to build other types of special entries. For example, to allow comment lines in our command language, we could add this entry:

    CmdFunc cmds[] = 
    {
      // ...
      CmdFunc("#", "  -- introduces comment line"),
      // ...
    };

When the first token of a command is #, we'll apply CmdFunc::null() to the command itself, which is a null operation -- just what we want.

Later on, we'll be listing out the commands and their help strings. To enhance readability of a long list, we can insert a blank line with an entry like this:

    CmdFunc cmds[] = 
    {
      // ...
      CmdFunc("", ""),
      // ...
    };

The "" can never match any command token, so the command will never execute; it's just a placeholder.

Searching the table

Once we have a CmdBuf holding a command and a list of CmdFunc objects, we need to isolate the command name, search the table for the command, and run the command handler. Continuing our example, the code will look like this:

    CmdBuf buf(is, os, "> ");

    SerialPort serialDriver(...);
    MotorDriver motorDriver(...);
    SystemVersion systemVersion(...);

    CmdFunc cmds[] = 
    {
      CmdFunc("baud", "    -- set baud rate", serialDriver, &SerialPort::baud),
      CmdFunc("parity", "  -- set parity", serialDriver, &SerialPort::parity),
      CmdFunc("data", "    -- set data bits", serialDriver, &SerialPort::dataBits),
      CmdFunc("stop", "    -- set stop bits", serialDriver, &SerialPort::stopBits),
      CmdFunc("speed", "   -- set motor speed", motorDriver, &MotorDriver::speed),
      CmdFunc("version", " -- show version", systemVersion, &SystemVersion::show),
      CmdFunc(0, "unknown command")
    };

    buf.search(cmds).parse(buf);    

CmdBuf::search() method looks for an entry in the command table cmds whose name field matches the first token in the CmdBuf. It returns the matching CmdFunc, or it returns the list terminator if no entry matches the token. CmdFunc::parse() method runs the selected CmdFunc's parser on its object, passing in the CmdBuf so the parser can retreive the command`s arguments (if any).

Start the search by extracting the first token from the CmdBuf:

  const CmdFunc& CmdBuf::search(CmdFunc* list, bool complain = true)
  {
    char token[20];
    in() >> setw(sizeof(token)) >> token;

>> setw(sizeof(token)) ensures that the token buffer won't be overrun, even if the actual token is too long to fit. >> token calls istream::operator >> (char*) to copy the next group of non-whitespace characters into the token buffer.

Now search the list, looking for an entry with a matching name field:

    const CmdFunc* cmd;
    for (cmd = list; cmd->name; cmd++)
      if (strcasecmp(cmd->name, token) == 0)
        break;

At this point, cmd points either to a matching entry or to the list terminator. Note that we can end up at the list terminator for several reasons: if the input stream is in a "failed" state, if there is no token in the stream, or if the token does not match.

This is a good time to offer to display the menu for the user. If the user enters "?" instead of a command, we dump the commands and their help strings to the console. As a special feature, don't show an entry if there's no help string; this allows for "hidden" commands.

    if (strcmp(token, "?") == 0)
    {
      for (const CmdFunc* cmd = list; cmd->name; cmd++)
        if (cmd->info)
          out() << "  " << cmd->name << cmd->info << endl;
      in().set(ios::failbit);
    }

After dumping the menu, mark the input stream "failed". Doing so will cause any further parsing of this input line to fail, avoiding confusion on complex command lines.

Finally, if the desired command was not found, display the list terminator's help string as an error message. As a special case, optionally (based on the complain argument) skip the error message if there was nothing at all on the input line. This special case allows the user to enter a blank line to obtain a new prompt without an annoying error message.

    else if (!cmd->name && (token[0] || complain))
      err() << cmd->info << endl;

We're finished, so return either a matching CmdFunc or the list terminator.

    return *cmd;
  }

Running the command handler

CmdBuf::search() always returns a reference to a CmdFunc object, whether or not it found the desired command in the table. Recall that all CmdFunc constructors ensure valid a object reference and parser pointer (even though the parser may be a null routine), so CmdFunc::parse() can simply apply the parser to the object:

    void parse(CmdBuf& buf) const { (object.*parser)(buf); }   

The "object.*parser" syntax says to look up the member function pointed to by parser and run it on object. As with regular function pointers, the parentheses around the pointer dereference are required because it has a lower operator precedence than the function call.

Generalizaing for other types of lists

In our serial port example, the parity setting should have only a few valid options: odd, even, mark, space, and none. To build a truly bullet-proof command-line interface, we should accept only these and reject all others. To assist the user, we should also give helpful error messages and allow listing the valid options.

With a few simple changes, we can reuse much of the command table machinery to offer support for enumerated variables like these. When we finish, we'll be able to write something like this:

    class SerialPort
    {
      enum Parity { odd, even, mark, space, none };
      Parity parity;
    
      // ...
 
    public:
      SerialPort() : parity(none), /*...*/ { }

      void parityCmd(CmdBuf& buf)
      {
        Cmd<Parity> opts[] =
        {
          Cmd<Parity>("odd", "   -- odd parity", odd),
          Cmd<Parity>("even", "  -- even parity", even),
          Cmd<Parity>("mark", "  -- parity bit low", mark),
          Cmd<Parity>("space", " -- parity bit high", space),
          Cmd<Parity>("none", "  -- no parity", none),
          Cmd<Parity>(0, "unknown option; try \"?\"", parity),
        };

        parity = buf.search(opts);
        // copy parity to hardware
      }

      // ...
    };

Note the similarity to command lists: we have a table of valid entries and we use CmdBuf::search() to select one. The form of the entries is different, however, and we use the result of the search differently as well.

We'd like to build enumerated lists with any type of target object, which a template allows us to do:

    template <typename T>
    class Cmd : public CmdBase
    {
      const T& object;

    public:
      Cmd(const char* n, const char* i, const T& t)
      : CmdBase(n, i), object(t)
      { }

      operator const T& const { return object; }
    };

In addition to the name and info fields in its CmdBase, an instance of the Cmd template holds a reference to an object (the object to choose if the name matches a token on the command line). Once we've used CmdBuf::search() to select Cmd, we can use an overloaded conversion operator to obtain access to the Cmd's object. For a given type T, we can obtain a Cmd<T>'s value by simply assigning the Cmd<T> object to a T object.

In our example, we wrote:

    Parity parity;

    //...

    parity = buf.search(opts);

The expression buf.search(opts) returns a Cmd<Parity> object. By "assigning" that object to a Parity object, we implicitly invoke the conversion operator, which returns a reference to the Cmd's Parity object, which is then copied into the Parity object parity;

Recall that CmdBuf::search() assumed it was searching a table of CmdFunc's, simply incrementing a CmdFunc pointer to obtain the next element in the table. Since CmdFunc's are not, in general, the same size as Cmd<>'s, we need to generalize search() to support tables of other types. We can do so by simply making search() a template.


  template <typename T>
  const T& CmdBuf::search(T* list, bool complain = true)
  {
    // as above, replacing "CmdFunc" with "T"
  }

Now we can search through lists of any class having name and info strings, such as all those derived from CmdBase.

This is one case where using a template will increase code size, but `search() is a reasonably small routine, and in a typical system the number of calls to it will typically be few. In any case, the code should be signficantly shorter than the if-else chain approach.

Conclusion

This article demonstrates how to apply C++ techniques to a real-world problem. The solution uses:

As used in this article, these techniques incur little or no run-time penalty, but they make the code shorter, clearer, more secure, and more robust under long-term maintenance. A future article on this topic will enhance the design with nested menus and nicer error display.

The full source code is available here: cmd.h and cmd.cpp. If you've found this article useful, we'd appreciate hearing from you. Please email the author.


Copyright 2014 Real-Time Systems Inc. All Rights Reserved.