Last Update:
Menu Class - Example of Modern C++17 STL features
Table of Contents
Writing articles about modern C++ features is a lot of fun, but what’s even better is to see how you use those new things in real world.
Today I’m happy to present a guest post article from JFT who was so kind
to describe his project where he uses several C++17 features.
He shared the process of building a menu that is based on std::any
,
std::variant
and std::optional
.
Have a look!
Background
This article arose from Bartek’s blog regarding
std::any
where he
asked for examples of usage. This followed his excellent series of
articles on the new C++17
std::any
,
std::variant
and
std::optional
features.
As I had already been ‘playing around’ with these when I was learning these new C++17 features (yes, we all have to do the book-work to learn new language features – knowledge suddenly doesn’t get implanted into us, even in Bjarne’s case!), and had produced some code that formed the basis of a command-line menu system as a non-trivial example, I posted a link to this code http://coliru.stacked-crooked.com/a/1a5939ae9ce269d2 as a comment to the blog. Bartek has kindly asked me to produce this guest blog describing this implementation.
Put Simply
What I developed is a very simple command-line menu class and associated utility functions. These utility functions provide the easy means to obtain console input – which as every C++ programmer knows – is fraught with issues regarding stream state etc etc etc for ‘bad input’.
Then there is the menu class. This enables menus to be created and linked together. A menu item displayed can be either a call to a specified function or to reference another menu – or to return to the previous menu if there was one. So the menus are sort of hierarchical.
Here’s a screenshot that illustrate how it looks like:
The Console Input Functions
These provide an easy means of obtaining different types of data from keyboard input – a string (whole line of data), a number (of different types and within optional specified ranges) and a single char (optionally restricted to a specified set of chars).
As it is common when obtaining console input to also need to display a message detailing the required input, these ‘high level’ routines also allow an optional message to be displayed, together with default input if just the return key is pressed. And they won’t return until valid input has been entered! They are as documented in the linked code.
However, these don’t actually undertake the work of obtaining the data –
they just display and check validity. The actual tasks of obtaining
console input are performed by a set of lower-level functions. These
deal with actually inputting the data, checking for bad stream state
etc. These have a return type of optional<T>
where if the input is
good (eg a number has been entered) then a value is returned, but if the
input was ‘bad’ then no value is returned.
For entering numeric data, the default way is to obtain a whole line of input data and then converting this (or attempting to convert) to a number of the specified type. This conversion code is:
template<typename T = int>
bool startsWithDigit(const std::string& s)
{
if (s.empty())
return false;
if (std::isdigit(s.front()))
return true;
return (((std::is_signed<T>::value
&& (s.front() == '-')) || (s.front() == '+'))
&& ((s.size() > 1) && std::isdigit(s[1])));
}
template<typename T = int>
std::optional<T> stonum(const std::string& st)
{
const auto s = trim(st);
bool ok = startsWithDigit<T>(s);
auto v = T {};
if (ok) {
std::istringstream ss(s);
ss >> v;
ok = (ss.peek() == EOF);
}
return ok ? v : std::optional<T> {};
}
Where st
is the string to convert. This first part removes leading and
trailing white-space characters and then attempts to convert the whole
of the number represented by s
to a numeric of type T
.
The conversion is performed by using stream extraction for the required
type from a stringstream
object.
As a number can be preceded by a ‘+’ and a signed number can be preceded by a ‘-‘, this is first checked – as an unsigned number is allowed to be converted with a leading ‘-‘ using stream extraction – it just gets converted to a very large positive number! If the number is valid, then an optional value is returned – otherwise, no value is returned.
Note that all of the characters in s
have to represent a valid number.
So “123”, “123”, “+123” are valid but “123w” or “q12” are not. To
determine if all characters have been successfully converted, .peek()
is used on the stream to obtain the current character after the
conversion. If the current stream index is at the end (ie all characters
have been successfully converted), then .peek()
will return EOF
. If
there was a problem converting one of the characters then .peek()
will
return this bad character – which won’t be EOF
.
Note that this method of conversion using stream extraction is very slow compared to other methods. However, in the case of console input, this is unlikely to be an issue – as I can’t see people typing faster than the speed of the conversion!
The Menu Class
As I said earlier, this is a simple console menu system. The heart of
which revolves around the Menu
class.
A menu consists of one or more menu items – which can either be a function pointer or a pointer to another menu. As two different types of entry are to be stored, it made sense to have a vector of variant as the two types are known.
Well not quite. The type of pointer to menu is certainly known, but a pointer to function? No – as the type depends upon the function arguments.
As the menu is divorced from the functions it calls and doesn’t know anything about them, it doesn’t know the function parameters used - that is known to the function writers.
So it was decided that the functions called would only have one
parameter - but which would be defined by the menu users. So std::any
type was used for the function parameter so the type of entry for the
function is known. Hence all functions have the declaration:
void f1(any& param);
Giving a function type of:
using f_type = void(*)(std::any& param);
All functions called must have this same signature. If more than one
parameter would be required for the functions, then the type for any
could be a struct
etc – or any type really. That is the beauty of
std::any
!
The two types required to be stored for the vector menu are, therefore
f_type
and Menu*
. Hence the structure of a menu item is:
struct MenItm
{
std::string name;
std::variant<f_type, menu*> func;
};
Internally, the Menu
class uses a vector to store the contents of the
menu, so this vector is just a vector of type MenItm
. Hence within the
main menu()
function of the class Menu
, it then becomes quite
simple.
First, the menu is displayed using a lambda and a valid option obtained.
Option 0
always means terminate that menu and either return to the
previous one or exit. If the option isn’t 0
then determine whether it
is a function pointer. If it is, execute the function. If it is not,
then call the specified menu object. To display and obtain a valid
option as part of the lambda show()
is just:
getnum<size_t>(oss.str(), 0, nom)
where oss
has been constructed previously. 0
is the minimum allowed
value and nom
is the maximum allowed. Given this, to display and
process a menu and its entered valid option is simply:
for (size_t opt = 0U; (opt = show(m)) > 0;)
{
if (const auto& mi = m.mitems[opt - 1];
std::holds_alternative<Menu::f_type>(mi.func))
{
std::get<Menu::f_type>(mi.func)(param);
}
else
{
menu(*std::get<Menu*>(mi.func), param);
}
}
A Structured Binding could have been used for the value of .mitems[]
,
but as only .func
is required it didn’t seem worth it.
As the type of the parameters passed between the various functions is not a part of the menu system but of the functions, this type should be defined before the functions are defined as:
using Params = <<required type>>;
// This then gives the start of the functions as:
void func(any& param)
{
auto& funcparam = any_cast<Params&>(param);
// Rest of function using funcparam
}
The Example
The example used here to demonstrate the input functions and the menu
class is a simple two-level menu that allows data of different types
(char
, signed int
, unsigned int
, double
and string
) to be
entered and stored in a single vector. As this vector needs to be passed
between the various functions called from the menu, the type Params
is
defined for this example as:
using Params = vector<variant<size_t, int, double, char, string>>;
which gives v
as the vector of the specified variants as required.
push_back()
is then used in the various functions to push the required
value onto the vector. For example:
void f6(any& param)
{
auto& v = any_cast<Params&>(param);
v.push_back(getnum<double>("Enter a real between", 5.5, 50.5));
}
Which asks the user to enter a real number between the specified values
(and accepts the input, checks its validity, displays an error message
if invalid and re-prompts the user) and stores this number in the
vector. Note that getnum()
doesn’t return until a valid number has
been entered.
For f5()
, which displays the data from the vector, this simply tests
the type of data stored for each of the vector elements and displays it
using the standard stream insertion:
for (const auto& d : v)
{
if (auto pvi = get_if<int>(&d))
cout << *pvi << endl;
else
if (auto pvd = get_if<double>(&d))
cout << *pvd << endl;
...
The Visitor
The code in f5()
looks messy with deeply nested if-statements!
Is there a better way this can be coded?
Indeed there is using a C++17 function called std::visit()
. This
wasn’t used in the original code as at the time I hadn’t quite gotten
around to learning about it (I did say I wrote this code when I was
learning C++17 features!).
When Bartek reviewed this article, he suggested that I change this to
use std::visit()
which I have now done. This revised code can be found
at http://coliru.stacked-crooked.com/a/2ecec3225e154b65
Now for f5()
, the new code becomes
void f51(any& param)
{
const static auto proc = [](const auto& val) {
cout << val << endl;
};
auto& v = any_cast<Params&>(param);
cout << "Entered data is\n";
for (const auto& d : v)
visit(proc, d);
}
Which is a lot cleaner!
std::visit()
is a very powerful tool in C++17 and anyone who does much
programming using std::variant
should get to grips with it.
Its basic usage is quite simple. In the above the variable d
(which
don’t forget is a variant) is processed (ie visited) by the lambda
proc
. The lambda itself is also quite simple: It takes an auto type
parameter and displays its content using cout
. This is a generic
lambda (introduced in C++14) that allows different types to be passed -
which is just what we need as std::cout
works with various types.
The parameter val
will be one of the allowed variant types.
The important point to note about using a lambda with std::visit()
is
that the code for each of the possible variant types should be the same
– as it is here.
The other part of the code which depends upon the type of the variant
is, of course, that which processes a menu item. The original code is
shown above within the discussion of the Menu class. Again, this could
use std::visit()
. The revised code using this is:
class RunVisitor
{
public:
RunVisitor(std::any& par) : param(par) {}
void operator()(f_type func) { func(param); }
void operator()(Menu* menu) { Menu::menu(*menu, param); }
private:
std::any& param;
};
// ...
for (size_t opt = 0U; (opt = show(m)) > 0; )
std::visit(RunVisitor(param), m.mitems[opt - 1].func);
While the body of the for loop is more concise, there is the extra class
RunVisitor
required in this case. This is because the processing
required for the different variant types is not the same – as it was
when used for f51()
. So a simple lambda cannot be used here, and hence
we need to fall-back to the old functor. For this functor
(RunVisitor
), an operator()
needs to be specified for each of the
different variant types. In this case for type f_type
, call the
function and for type Menu*
, call the menu function.
Note that for std::visit()
, the functor/lambda (Callable in C++17
terms) is the first parameter of visit – unlike other Standard Library
functions when this is usually the last parameter. This is because more
than one parameter may be passed to the Callable.
Play With the Code
The code can be found @Coliru
But, below you can also play live with it (and even work in a terminal! (sessions are scheduled to last max 60 seconds):
In conclusion
It is of course, up to the user of Menu to determine the menu structure
and the type used with any<>
as specified by Params. But if a quick
console application is needed that uses a menu and console input, then
this class and the various console input utility functions may help to
reduce the required effort. But in the age of touch-screen smartphones
and tablets, who would? - Maybe 35 years ago…… But as I said at the
beginning, this started as just a programming exercise.
Have fun!
More From the Guest Author
JFT recently also wrote a viral article @fluentcpp where he described his top 3 C++17 features: see it here: 3 Simple C++17 Features That Will Make Your Code Simpler.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: