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.
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
.
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.
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".
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()
.
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.
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.
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.
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.
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!
};
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
// ...
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.
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;
}
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.
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
;
search()
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.
This article demonstrates how to apply C++ techniques to a real-world problem. The solution uses:
iostream
's for string parsing and formatting
member-function pointers to avoid virtual functions and an explosion of trivial derived classes
a template constructor to ensure type-matching of arguments before casting
multiple constructors to cleanly construct special-case objects
inheritance to abstract common code
custom type conversions
new-style casts
placement new
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.