Some time ago, I wrote this answer to a question about creating a command line menu. I referred to it recently and noticed a few things that I wanted to improve.
Purpose
As with the original version, the purpose is to have a class which simplifies the construction and use of a command line (console) menu.
The improvements I made are:
- allow either
std::string
orstd::wstring
prompts and answers - allow the user to separate selectors from descriptions
- move everything into a header-only module
- allow creation of
const
menus - sort by selectors
Questions
Some things I had questions about are:
- template parameter names - could they be improved?
- use of
default_in
anddefault_out
- would it be better to infer defaults from the string type? - choice of
std::function<void()>
as the operation for each choice - use of
std::pair
vs. custom object - should I wrap all of this in a namespace?
- is any functionality missing?
- is there a way to make a
constexpr
version?
menu.h
#ifndef MENU_H
#define MENU_H
#include <functional>
#include <iostream>
#include <map>
#include <string>
#include <utility>
template <typename T> struct default_in;
template<> struct default_in<std::istream> {
static std::istream& value() { return std::cin; }
};
template<> struct default_in<std::wistream> {
static std::wistream& value() { return std::wcin; }
};
template <typename T> struct default_out;
template<> struct default_out<std::ostream> {
static std::ostream& value() { return std::cout; }
};
template<> struct default_out<std::wostream> {
static std::wostream& value() { return std::wcout; }
};
template <class str, class intype, class outtype>
class ConsoleMenu {
public:
ConsoleMenu(const str& message,
const str& invalidChoiceMessage,
const str& prompt,
const str& delimiter,
const std::map<str, std::pair<str, std::function<void()>>>& commandsByChoice,
intype &in = default_in<intype>::value(),
outtype &out = default_out<outtype>::value());
void operator()() const;
private:
outtype& showPrompt() const;
str message;
str invalidChoiceMessage_;
str prompt;
str delimiter;
std::map<str, std::pair<str, std::function<void()>>> commandsByChoice_;
intype ∈
outtype &out;
};
template <class str, class intype, class outtype>
ConsoleMenu<str, intype, outtype>::ConsoleMenu(const str& message,
const str& invalidChoiceMessage,
const str& prompt,
const str& delimiter,
const std::map<str, std::pair<str, std::function<void()>>>& commandsByChoice,
intype &in, outtype& out) :
message{message},
invalidChoiceMessage_{invalidChoiceMessage},
prompt{prompt},
delimiter{delimiter},
commandsByChoice_{commandsByChoice},
in{in},
out{out}
{}
template <class str, class intype, class outtype>
outtype& ConsoleMenu<str, intype, outtype>::showPrompt() const {
out << message;
for (const auto &commandByChoice : commandsByChoice_) {
out << commandByChoice.first
<< delimiter
<< commandByChoice.second.first
<< '\n';
}
return out << prompt;
}
template <class str, class intype, class outtype>
void ConsoleMenu<str, intype, outtype>::operator()() const {
str userChoice;
const auto bad{commandsByChoice_.cend()};
auto result{bad};
out << '\n';
while (showPrompt() && (!(std::getline(in, userChoice)) ||
((result = commandsByChoice_.find(userChoice)) == bad))) {
out << '\n' << invalidChoiceMessage_;
}
result->second.second();
}
#endif // MENU_H
main.cpp
#include "menu.h"
#include <iostream>
#include <functional>
template <class str, class outtype>
class Silly {
public:
void say(str msg) {
default_out<outtype>::value() << msg << "!\n";
}
};
using MySilly = Silly<std::string, std::ostream>;
int main() {
bool running{true};
MySilly thing;
auto blabble{std::bind(&MySilly::say, thing, "BLABBLE")};
const ConsoleMenu<std::string, std::istream, std::ostream> menu{
"What should the program do?\n",
"That is not a valid choice.\n",
"> ",
". ",
{
{ "1", {"bleep", []{ std::cout << "BLEEP!\n"; }}},
{ "2", {"blip", [&thing]{ thing.say("BLIP"); }}},
{ "3", {"blorp", std::bind(&MySilly::say, thing, "BLORP")}},
{ "4", {"blabble", blabble }},
{ "5", {"speak Chinese", []{std::cout << "对不起,我不能那样做\n"; }}},
{ "0", {"quit", [&running]{ running = false; }}},
}
};
while (running) {
menu();
}
}
This shows the use of the program and several different ways of creating menu functions. Depending on your console and compiler settings, the Chinese sentence may or may not be displayed properly. Next is a wide string version.
wide.cpp
#include "menu.h"
#include <iostream>
#include <functional>
#include <locale>
template <class str, class outtype>
class Silly {
public:
void say(str msg) {
default_out<outtype>::value() << msg << "!\n";
}
};
using MySilly = Silly<std::wstring, std::wostream>;
int main() {
bool running{true};
MySilly thing;
auto blabble{std::bind(&MySilly::say, thing, L"BLABBLE")};
ConsoleMenu<std::wstring, std::wistream, std::wostream> menu{
L"What should the program do?\n",
L"That is not a valid choice.\n",
L"> ",
L". ",
{
{ L"1", {L"bleep", []{ std::wcout << L"BLEEP!\n"; }}},
{ L"2", {L"blip", [&thing]{ thing.say(L"BLIP"); }}},
{ L"3", {L"blorp", std::bind(&MySilly::say, thing, L"BLORP")}},
{ L"4", {L"blabble", blabble }},
{ L"5", {L"说中文", []{std::wcout << L"对不起,我不能那样做\n"; }}},
{ L"0", {L"quit", [&running]{ running = false; }}},
}
};
std::locale::global(std::locale{"en_US.UTF-8"});
while (running) {
menu();
}
}
1 Answer 1
Answers to your questions
template parameter names - could they be improved?
Mostly it's that they are inconsistent. Start type names with a capital, and either suffix them all with Type
or don't. I suggest:
str
->Str
intype
->IStream
(just to be clear the we do expect something likestd::istream
here)outtype
->OStream
use of default_in and default_out - would it be better to infer defaults from the string type?
Yes, see below.
choice of
std::function<void()>
as the operation for each choice
You need std::function<>
here to store the functions for each choice in the map. The only question is if void()
is the right type for the function. If you wanted operator()()
to take parameters and/or return a value, then you would have to change the type of the function as well.
use of std::pair vs. custom object
I personally think it's fine with std::pair
.
should I wrap all of this in a namespace?
If it is just class ConsoleMenu
, I don't think it would be any improvement to put it in a namespace. However, I would put default_in
and default_out
in a namespace, as those names are quite generic, and you don't want them to pollute the global namespace.
is any functionality missing?
I don't know, if this is all you need then it's complete. If you need something else from it, it's not.
is there a way to make a constexpr version?
Yes, by making sure it satisfies the requirements of LiteralType. This also means that all member variables must be valid LiteralTypes, and that prevents using std::string
or std::map
. You can use const char *
and std::array
instead.
Pass the input and output stream by value
The construction you have where you pass a stream type as a template parameter, and then have it deduce a concrete stream from that is very weird, inflexible, and requires more typing than necessary. Just add the input and output stream as parameters to the constructor:
template <class str, class intype, class outtype>
class ConsoleMenu {
public:
ConsoleMenu(const str& message,
...,
intype &in,
outtype &out);
Compare:
ConsoleMenu<std::wstring, std::wistream, std::wostream> menu{...}
Versus:
ConsoleMenu<std::wstring> menu{..., std::wcin, std::wcout}
If you want the standard input and output to be a default parameter, then I would deduce it from the string type:
template <typename T> struct default_in;
template<> struct default_in<std::string> {
static std::istream& value() { return std::cin; }
};
template<> struct default_in<std::wstring> {
static std::wistream& value() { return std::wcin; }
};
...
template <class str, class intype, class outtype>
class ConsoleMenu {
public:
ConsoleMenu(const str& message,
...,
intype &in = default_in<str>::value(),
outtype &out = default_out<str>::value());
Because then you can just write:
ConsoleMenu menu{L"Wide menu", L"invalid", L"> ", L". ", {/* choices */}};
-
\$\begingroup\$ I also noticed another flaw with my version. If I try to use
std::string_view
, the code won't compile because it tries to read one fromstd::cin
. I think I'll have to key the type off of the string type such thatwstring_view
becomeswstring
. Still pondering how best to do that. \$\endgroup\$Edward– Edward2020年12月20日 14:51:33 +00:00Commented Dec 20, 2020 at 14:51 -
1\$\begingroup\$ You could use
str::value_type
perhaps? And then useintype = std::basic_istream<str::value_type>
as the default parameter value. \$\endgroup\$G. Sliepen– G. Sliepen2020年12月20日 16:34:25 +00:00Commented Dec 20, 2020 at 16:34
class
to maintain whatever shared state might be needed. Therunning
variable was intended to illustrate that kind of use. \$\endgroup\$