Simple interactive menu

deceptikon 2 Tallied Votes 3K Views Share

C++ conversion of this C snippet for a simple interactive menuing system.

The code shows three files:

  • menu.h: The header file for the menu class.
  • menu.cpp: Implementation of the menu class.
  • main.cpp: A sample driver that uses the library.

The menu library is fairly generic in that you can provide a format string of menu options and possible matching input values. See main.cpp for an example of how to use it. Note that C++11 features are used in this code.

mike_2000_17 commented: Cool! +13
// menu.h
#ifndef MENU_H
#define MENU_H

#include <string>
#include <vector>

namespace jrd {
    class Menu final {
    public:
        explicit Menu(const std::string& options);
        bool operator()(std::string& selection) const;
    private:
        struct Item {
            Item(const std::string& prompt = "")
                : prompt(prompt)
            { }

            std::vector<std::string> alt;
            std::string prompt;
        };

        std::vector<Item> _items;
    };
}

#endif

// menu.cpp
#include <algorithm>
#include <iostream>
#include <iterator>
#include <regex>
#include <string>
#include <vector>
#include "menu.h"

namespace jrd {
    /*
        @description:
            Parses and extracts a collection of menu items from a formatted options string.

            The options string shall be in the following format:
            
                <prompt1>;<prompt1.match1>|<prompt1.match2>;<prompt2>;<prompt2.match1>|<prompt2.match2>...

            The prompt may not be an empty field, but the match strings may be empty.

            Example 1 (prompts and matches):    "A) Option A;A|a;B) Option B;B|b;Q) Quit;Q|q"
            Example 2 (prompts and no matches): "A) Option A;;B) Option B;;Q) Quit;"
    */
    Menu::Menu(const std::string& options)
    {
        std::regex tok_regex("[;]");
        std::sregex_token_iterator first(options.begin(), options.end(), tok_regex, -1);
        std::sregex_token_iterator last;

        for (; first != last; ++first) {
            Item item(*first++);           // Assume the first hit is the prompt
            std::string alt_list = *first; // Assume the second hit is the alt list
            std::regex alt_regex("[|]");
            std::sregex_token_iterator alt_first(alt_list.begin(), alt_list.end(), alt_regex, -1);

            while (alt_first != last)
                item.alt.push_back(*alt_first++);

            _items.push_back(item);
        }
    }

    /*
        @description:
            Displays the prompts for a collection of menu items and gathers an interactive selection.

        @returns: True if a matching selection was detected, false otherwise.
    */
    bool Menu::operator()(std::string& selection) const
    {
        // Display the prompts
        for (auto x : _items)
            std::cout << x.prompt << '\n';

        std::cout << "> ";

        // Process an interactive selection
        if (getline(std::cin, selection)) {
            auto prefixed = [=](const std::string& s) {
                // If the first part of the string matches an alternative, it's valid
                return s.compare(0, selection.size(), selection) == 0;
            };

            for (auto x : _items) {
                // Check all of the alternatives for a matching prefix
                if (std::any_of(std::begin(x.alt), std::end(x.alt), prefixed))
                    return true;
            }
        }

        return  false; // No matches, end-of-file, or stream error
    }
}

// main.cpp (sample usage)
#include <cctype>
#include <iostream>
#include <string>
#include "menu.h"

int main()
{
    jrd::Menu menu("A) Option A;A|a;B) Option B;B|b;Q) Quit;Q|q");
    bool done = 0;
    
    while (!done) {
        std::string selection;
        bool rc = menu(selection);

        std::cout << "Full selection: '" << selection << "'\n";

        if (!rc)
            std::cout << "Invalid selection\n";
        else {
            switch (toupper(selection[0])) {
            case 'A':
                std::cout << "You chose option A\n";
                break;
            case 'B':
                std::cout << "You chose option B\n";
                break;
            case 'Q':
                std::cout << "You chose to quit\n";
                done = 1;
                break;
            }
        }
    }
}
mvmalderen 2,072 Postaholic

I am wondering if the Builder pattern would be a candidate for being applied here?
Let me know your opinion.

deceptikon 1,790 Code Sniper Team Colleague Featured Poster

I am wondering if the Builder pattern would be a candidate for being applied here?

Maybe you're thinking of doing it in a cool way (if so, please share!), but I can't think of any benefit to the Builder pattern that would justify the added complexity. Of course, I'm not a fan of design patterns in the first place, so I'm a little biased. ;)

CGSMCMLXXV 5 Junior Poster in Training

I would go for Interpreter (quite a basic one, though) + Command. Builder doesn't seem to be appropriate because the menu items are independent (even if you can extend the string defining the menu). My 2c opinion.

deceptikon 1,790 Code Sniper Team Colleague Featured Poster

I'd love to see implementations of these pattern based solutions. I'm still not seeing any benefit.

mike_2000_17 2,669 21st Century Viking Team Colleague Featured Poster

That's a neat code snippet!

Wouldn't it be nicer if the selection string was set to the given menu item (by name or ordinal) such that you would need to rely on the alternative match-strings for the switch case? In your test case, the alternatives are always an upper- or lower-case letter. What if you wanted alternatives like "quit" or "exit" for the quit option? That would result in two case entries (or two else-if blocks in this case).

You could have this selection-test loop:

        for (auto x : _items) {
            // Check all of the alternatives for a matching prefix
            if (std::any_of(std::begin(x.alt), std::end(x.alt), prefixed)) {
                selection = x.prompt;
                return true;
            }
        }

Then, you can do the test for selection as:

    if (!rc)
        std::cout << "Invalid selection\n";
    else {
        if( selection == "A) Option A" ) 
            std::cout << "You chose option A\n";
        else if( selection == "B) Option B" )
            std::cout << "You chose option B\n";
        else if( selection == "Q) Quit" ) {
            std::cout << "You chose to quit\n";
            done = 1;
        };
    }

I think it is desirable to make the content of selection (or whatever else identifies the option selected) have a unique value for each option, to make it more convenient and predictable for the user of the Menu class. A numeric value for the selection would also do nicely:

std::ptrdiff_t Menu::operator()() const
{
    // Display the prompts
    for (auto x : _items)
        std::cout << x.prompt << '\n';

    std::cout << "> ";

    // Process an interactive selection
    std::string selection_str;
    if (getline(std::cin, selection_str)) {
        auto prefixed = [=](const std::string& s) {
            // If the first part of the string matches an alternative, it's valid
            return s.compare(0, selection_str.size(), selection_str) == 0;
        };

        for (auto it = _items.begin(); it != _items.end(); ++it) {
            // Check all of the alternatives for a matching prefix
            if (std::any_of(std::begin(it->alt), std::end(it->alt), prefixed))
                return it - _items.begin();
        }
    }
    return -1; // No matches, end-of-file, or stream error
}

And then switch based on the item index (or use the index to retrieve the prompt string and do an else-if chain based on that).

The way I see it, the main work being done with this Menu class is to match the alternatives to whatever the user has typed. But returning the string that the user has typed, unmodified, from the function means that this work must essentially be done again, with the only benefit of knowing in advance that there is a match or not. So, one way or another, you must preserve that work and carry the critical information out of the function in a way that doesn't force the user to re-do the same alternatives-matching work. Otherwise, there isn't much difference between your code and this:

int main()
{
    bool done = 0;

    while (!done) {
        std::string selection;
        std::cout << "A) Option A\nB) Option B\nQ) Quit\n> ";
        std::getline(std::cin, selection);

        switch (toupper(selection[0])) {
        case 'A':
            std::cout << "You chose option A\n";
            break;
        case 'B':
            std::cout << "You chose option B\n";
            break;
        case 'Q':
            std::cout << "You chose to quit\n";
            done = 1;
            break;
        default:
            std::cout << "Invalid selection\n";
        }
    }
}
mvmalderen commented: Nice catch. +13
deceptikon commented: I like that. +12
mike_2000_17 2,669 21st Century Viking Team Colleague Featured Poster

Here is an alternative for the Menu class that might be a bit more convenient (and I guess it might qualify as a Builder pattern, not sure as I, like deceptikon, don't care too much about these things).

// menu.h
#ifndef MENU_H
#define MENU_H

#include <string>
#include <vector>
#include <functional>

namespace jrd {
    class Menu final {
    public:
        Menu& addOption(const std::string& aPrompt,
                        const std::string& aAlternativeMatches,
                        std::function< void() > aCallback);
        bool execute() const;
    private:
        struct Item {
            template <typename ForwardIter>
            Item(const std::string& aPrompt,
                 std::function< void() > aCallback,
                 ForwardIter first, ForwardIter last)
                : alt(first,last), prompt(aPrompt), callback(aCallback)
            { }

            std::vector<std::string> alt;
            std::string prompt;
            std::function< void() > callback;
        };

        std::vector<Item> _items;
    };
}

#endif

// menu.cpp
#include <algorithm>
#include <iostream>
#include <iterator>
#include <regex>
#include <string>
#include <vector>
#include "menu.h"

namespace jrd {
    /*
        @description:
            Creates a menu-item from a prompt string, a formatted set of 
            alternative matches, and callback functor.

            The alternative matches string shall be in the following format:

                <match1>|<match2>...

            The prompt may not be an empty field, but the alternatives may be empty.
    */
    Menu& Menu::addOption(const std::string& aPrompt,
                          const std::string& aAlternativeMatches,
                          std::function< void() > aCallback)
    {
        std::sregex_token_iterator alt_first(aAlternativeMatches.begin(), 
                                             aAlternativeMatches.end(), 
                                             std::regex("[|]"), -1);
        std::sregex_token_iterator alt_last;
        _items.push_back(Item(aPrompt, aCallback, alt_first, alt_last));
        return *this;
    }

    /*
        @description:
            Displays the prompts for a collection of menu items and 
            executes the appropriate callback functor.

        @returns: True if a matching selection was detected, false otherwise.
    */
    bool Menu::execute() const
    {
        // Display the prompts
        for (auto x : _items)
            std::cout << x.prompt << '\n';

        std::cout << "> ";

        std::string selection;
        // Process an interactive selection
        if (getline(std::cin, selection)) {
            auto prefixed = [=](const std::string& s) {
                // If the first part of the string matches an alternative, it's valid
                return s.compare(0, selection.size(), selection) == 0;
            };

            for (auto x : _items) {
                // Check all of the alternatives for a matching prefix
                if ( prefixed(x.prompt) || 
                     std::any_of(std::begin(x.alt), std::end(x.alt), prefixed) ) {
                    x.callback();  // call the functor.
                    return true;
                };
            }
        }
        return  false; // No matches, end-of-file, or stream error
    }
}

// main.cpp (sample usage)
#include <cctype>
#include <iostream>
#include <string>
#include "menu.h"

int main()
{
    bool done = 0;

    jrd::Menu menu;
    menu.addOption( "A) Option A", "A|a", []() {
        std::cout << "You chose option A" << std::endl;
    }  ).addOption( "B) Option B", "B|b", []() {
        std::cout << "You chose option B" << std::endl;
    }  ).addOption( "Q) Quit",     "Q|q", [&done]() {
        std::cout << "You chose to quit" << std::endl;
        done = true;
    });

    while (!done) {
        if (!menu.execute())
            std::cout << "Invalid selection" << std::endl;
    }
}
mvmalderen 2,072 Postaholic

Mike, it's almost like you can read my mind :)
While reading your previous post I was thinking of replacing the switch statement by callbacks.
Glad to see that idea confirmed.

deceptikon 1,790 Code Sniper Team Colleague Featured Poster

menu.addOption( "A) Option A", "A|a", {...

Elegant and nifty, but it's kind of ugly, no? ;)

mike_2000_17 2,669 21st Century Viking Team Colleague Featured Poster

but it's kind of ugly, no? ;)

And jrd::Menu menu("A) Option A;A|a;B) Option B;B|b;Q) Quit;Q|q"); is beautiful?

I guess one could use a few MACROs too:

#define JRD_MENU_IF(MENU, X, Y) jrd::Menu MENU; MENU.addOption(X, Y, [&]() 
#define JRD_MENU_ELSE_IF(X, Y) ).addOption(X, Y, [&]() 
#define JRD_MENU_END_IF )

And then have the menu creation be like this:

    JRD_MENU_IF(menu,  "A) Option A", "A|a") {
        std::cout << "You chose option A" << std::endl;
    } JRD_MENU_ELSE_IF("B) Option B", "B|b") {
        std::cout << "You chose option B" << std::endl;
    } JRD_MENU_ELSE_IF("Q) Quit",     "Q|q") {
        std::cout << "You chose to quit" << std::endl;
        done = true;
    } JRD_MENU_END_IF;

But I guess that won't satisfy you esthetic requirements either. ;)

Joking aside, this pattern of creating the object by several calls like the addOption() function above is a bit weird-looking, but it is very useful. This pattern is predominant in libraries like Boost.Program-Options and Boost.Parameter. I also use it often for creating objects that contain many parameters and can have many options and variations, especially manufacturing objects from a class template with many optional arguments simply by returning an object of a different type at every step. For example, this version giving a static number of menu options with templated callback functors:

template <typename... Functors>
class Menu final {
public:
    // ...

    template <typename NewFunctor>
    Menu< Functors... , NewFunctor >
      addOption(const std::string& aPrompt,
                const std::string& aAlternativeMatches,
                NewFunctor aCallback) const;

    // ...
private:
    std::array< std::string, sizeof...(Functors) > prompts;
    std::array< std::vector<std::string>, sizeof...(Functors) > matches;
    std::tuple< Functors... > callbacks;
};

// use it like this:
auto menu = jrd::Menu<>()
.addOption( "A) Option A", "A|a", []() {
    std::cout << "You chose option A" << std::endl;
})
.addOption( "B) Option B", "B|b", []() {
    std::cout << "You chose option B" << std::endl;
})
.addOption( "Q) Quit",     "Q|q", [&done]() {
    std::cout << "You chose to quit" << std::endl;
    done = true;
});

In this example, it's more trouble than it's worth, but in other contexts it's nice.

deceptikon 1,790 Code Sniper Team Colleague Featured Poster

And jrd::Menu menu("A) Option A;A|a;B) Option B;B|b;Q) Quit;Q|q"); is beautiful?

I'd argue that flat file formats (of which that format string is derived) are well understood to the point where it's not excessively krufty. Further, the kruft is limited to the string and doesn't involve what I'd consider to be an unconventional coding style as in the chain of addOption() calls.

But I guess that won't satisfy you esthetic requirements either. ;)

Actually, it looks more conventional, but it's even worse because the macros lie about what's happening. You're not executing an if chain, you're building a menu inside an object. Given the choice of the original code and the macros, I'd pick the original because it's more clear what the intended behavior is.

I know that was a joke, but a serious response may help elaborate on my reasoning. ;)

this pattern of creating the object by several calls like the addOption() function above is a bit weird-looking, but it is very useful.

I don't disagree that it can be useful, but weird looking code should be limited to places where it's genuinely a very superior option because it's weird looking. I'm a strong proponent of transparency at a glance. If I can just look at a piece of code and have a good idea of how execution flows, it's transparent. If I have to linger and mentally parse what's happening then it's not transparent.

That's actually my beef with modern C++ (template metaprogramming being a good example). It's expert friendly to a fault these days, so I strive to keep my code as simple as possible while still adopting the more useful new stuff (eg. lambdas).

This pattern is predominant in libraries like Boost.Program-Options and Boost.Parameter.

You'll be unsurprised to learn that I find Boost to be ugly in general. Only the simplest libraries like lexical_cast have made it into regular use in my own code, and even then I often simulate them. I've worked with both of the libraries you mentioned and they're both an excellent example of ugly complexity. Boost is a library to be certain (a library by experts and seemingly for experts), but it's also a testing ground for pushing the limits of what C++ offers. In my opinion those two goals get mixed up too much for Boost to be the CPAN of C++.

mike_2000_17 2,669 21st Century Viking Team Colleague Featured Poster

I largely agree with you about modern C++ being too expert-friendly and not accessible enough to others. I think the core of the issue is some libraries focus more on features than on useability. It is easy, when you use template meta-programming or advanced generic programming techniques, to get caught up in "feature-building", and forget that the stuff needs to be relatively intuitive to use and predictable, well-specified, etc... And certainly, some Boost libraries tip a bit too far towards just being packed full of customizable behaviors and features, and at the end of the day, you can't use much of it because it lacks the most basic user-oriented qualities.

For Boost.Program-Option, I actually think it's pretty good (not perfect, of course). It is a small and simple library which was clearly designed with useability concerns in mind as the main objective of it. I think there are a few things that went a bit too far in it, making it a bit too complex in certain areas. You can argue that it is not used in an idiomatic way, and you might judge that as ugly, but that's a constant struggle in a multi-paradigm language like C++.

We've had at least a decade (90s to early 2000s) with a substantial group of people insisting on making C++ an "object-oriented language" and obey to practices similar to what you find in Java today (dating back to Simula). And they cried foul anytime they saw "ugly, complex, expert-only" template code, even when the code was in a very humble STL-like style with a limited amount of bells and whistles. And some awkward libraries resulted from that, e.g., OpenInventor is one classic example. Only in the past 5-10 years, C++ programmers have started to reclaim the right to set the coding practices that make sense for C++ in all its glory (unchained!). And that has opened things up for exploration of different paradigms that are often a mix of different ideas, sometimes with good results, sometimes not. Boost is a distillery for that.

What I'm getting at is that we're in a transient period right now, and things are brewing hard (and the whole C++0x tribulations were a prime sign of that). So, it's not a very good time to be clinging to orthodoxies. And yes, it makes a library collection like Boost become a bit chaotic, i.e., almost every individual library in it seems to setup its own paradigm. But you also have to be open to new ways of thinking about the code. For example, lately, declarative-programming ideas have been popping up all over the place in the world of C++, which is in stark contrast to the orthodoxies of PP / OOP / GP. Like in that menu example which is in two steps: declare what you want to happen; and, set it in motion. It looks odd if you think about code in procedural terms, but that's in the eye of the beholder. C++, with RAII, is actually a really good language for declarative programming, so we've been seeing a lot of that lately.

In my opinion those two goals get mixed up too much for Boost to be the CPAN of C++.

To me, Boost is more of a place that I look for ideas and techniques, more than I actually use the libraries themselves. Most of the Boost libraries I use (and have ever used) on a daily basis are those that are now in C++11. One exception is the Boost Graph Library, which I have extended as well. I don't think that Boost is or should be viewed as the CPAN of C++. More like a laboratory, which is just to say that it's a playground with expert supervision. The libraries get distilled, and those whose paradigms become very popular end up being promoted to standard libraries, like smart-pointers, threading and regex. C++ is very peculiar because it has such a heavy baggage already, and yet it still has a lot of dynamic stuff going on upstream.

deceptikon 1,790 Code Sniper Team Colleague Featured Poster

So, it's not a very good time to be clinging to orthodoxies.

Make no mistake, I'm very open to new concepts and ways of making things better. For example, I have used both Boost.Program-Options and Boost.Parameter because they're good ideas and neat implementations. I've rejected them from a position of experience, it's not just some knee jerk reaction to all things new. In the pursuit of better methods we can't forget that what's worked in the past has worked for a reason.

More like a laboratory, which is just to say that it's a playground with expert supervision.

It seems we see it the same way. Sadly, we're in the minority. Just about everyone I talk to thinks Boost is essentially C++'s version of CPAN.

tinstaafl 1,176 Posting Maven

Perhaps I'm missing something, but why is:

int main()
{
    string selection = "";
    while (!(selection == "Q"))
    {
    cout <<"\n\n\tA)\tOption A\n\tB)\tOption B\n\tQ)\tQuit\n";
    cout << "Full selection: '";
    cin >> selection;
    selection = char(toupper(selection[0]));
        switch (selection[0]) 
        {
        case 'A':
            cout << "You chose option A\n";
            break;
        case 'B':
            cout << "You chose option B\n";
            break;
        case 'Q':
            cout << "You chose to quit\n";
            break;
        default:
            cout << "Invalid selection\n";
            break;
        }
    }
    return 0;
}

so horrible that a whole class is needed to replace 2 lines?

mike_2000_17 2,669 21st Century Viking Team Colleague Featured Poster

That's exactly the point I made a few posts back.

tinstaafl 1,176 Posting Maven

Sorry missed that.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.