Partners: KDAB Whole Tomato Software CppDepend

09 July 2018

Menu Class - Example of Modern C++17 STL features

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:

Simple Menu, C++17

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.

Get my free ebook about C++17!

More than 50 pages about the new Language Standard.

C++17 in detail, by Bartlomiej Filipek

© 2017, Bartlomiej Filipek, Blogger platform
Any opinions expressed herein are in no way representative of those of my employers.
This site contains ads or referral links, which provide me with a commission. Thank you for your understanding.