Generischer Konsolenmenühersteller nur für Header
Vor einiger Zeit habe ich diese Antwort auf eine Frage zum Erstellen eines Befehlszeilenmenüs geschrieben. Ich habe kürzlich darauf hingewiesen und einige Dinge bemerkt, die ich verbessern wollte.
Zweck
Wie bei der Originalversion besteht der Zweck darin, eine Klasse zu haben, die den Aufbau und die Verwendung eines Befehlszeilenmenüs (Konsole) vereinfacht.
Die Verbesserungen, die ich vorgenommen habe, sind:
- Erlaube entweder
std::string
oderstd::wstring
Aufforderungen und Antworten - Ermöglichen Sie dem Benutzer, Selektoren von Beschreibungen zu trennen
- Verschieben Sie alles in ein Nur-Header-Modul
- Ermöglichen die Erstellung von
const
Menüs - Sortieren nach Selektoren
Fragen
Einige Dinge, zu denen ich Fragen hatte, sind:
- Vorlagenparameternamen - könnten sie verbessert werden?
- Verwendung von
default_in
unddefault_out
- wäre es besser, Standardeinstellungen aus dem Zeichenfolgentyp abzuleiten? - Wahl
std::function<void()>
als Operation für jede Wahl - Verwendung von
std::pair
vs. benutzerdefinierten Objekt - soll ich das alles in einen Namespace einschließen?
- Fehlt eine Funktionalität?
- Gibt es eine Möglichkeit, eine
constexpr
Version zu erstellen ?
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();
}
}
Dies zeigt die Verwendung des Programms und verschiedene Möglichkeiten zum Erstellen von Menüfunktionen. Abhängig von Ihren Konsolen- und Compilereinstellungen wird der chinesische Satz möglicherweise nicht richtig angezeigt. Als nächstes folgt eine Version mit breiten Zeichenfolgen.
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();
}
}
Antworten
Antworten auf Ihre Fragen
Vorlagenparameternamen - könnten sie verbessert werden?
Meistens sind sie inkonsistent. Beginnen Sie Typnamen mit einem Großbuchstaben und setzen Sie sie entweder alle mit Type
oder nicht. Ich schlage vor:
str
->Str
intype
->IStream
(nur um klar zu sein, dass wir so etwas wiestd::istream
hier erwarten )outtype
->OStream
Verwendung von default_in und default_out - wäre es besser, Standardeinstellungen aus dem Zeichenfolgentyp abzuleiten?
Ja, siehe unten.
Wahl
std::function<void()>
als Operation für jede Wahl
Hier müssen Sie std::function<>
die Funktionen für jede Auswahl in der Karte speichern. Die Frage ist nur, ob void()
der richtige Typ für die Funktion ist. Wenn Sie operator()()
Parameter übernehmen und / oder einen Wert zurückgeben möchten, müssen Sie auch den Funktionstyp ändern.
Verwendung von std :: pair vs. benutzerdefiniertem Objekt
Ich persönlich denke, es ist in Ordnung mit std::pair
.
soll ich das alles in einen Namespace einschließen?
Wenn es nur so ist class ConsoleMenu
, denke ich nicht, dass es eine Verbesserung wäre, es in einen Namespace zu stellen. Allerdings würde ich setzen default_in
und default_out
in einem Namensraum, wie diese Namen ganz generisch sind, und Sie wollen nicht zu dem globalen Namensraum verschmutzen.
Fehlt eine Funktionalität?
Ich weiß nicht, wenn das alles ist, was Sie brauchen, dann ist es vollständig. Wenn Sie etwas anderes brauchen, ist es nicht.
Gibt es eine Möglichkeit, eine Constexpr-Version zu erstellen?
Ja, indem Sie sicherstellen, dass es den Anforderungen von LiteralType entspricht . Dies bedeutet auch, dass alle Mitgliedsvariablen gültige LiteralTypes sein müssen, und dies verhindert die Verwendung von std::string
oder std::map
. Sie können const char *
und std::array
stattdessen verwenden.
Übergeben Sie den Eingabe- und Ausgabestream als Wert
Die Konstruktion, in der Sie einen Stream-Typ als Vorlagenparameter übergeben und dann einen konkreten Stream daraus ableiten lassen, ist sehr seltsam, unflexibel und erfordert mehr Eingabe als erforderlich. Fügen Sie dem Konstruktor einfach den Eingabe- und Ausgabestream als Parameter hinzu:
template <class str, class intype, class outtype>
class ConsoleMenu {
public:
ConsoleMenu(const str& message,
...,
intype &in,
outtype &out);
Vergleichen Sie:
ConsoleMenu<std::wstring, std::wistream, std::wostream> menu{...}
Gegen:
ConsoleMenu<std::wstring> menu{..., std::wcin, std::wcout}
Wenn Sie möchten, dass die Standardeingabe und -ausgabe ein Standardparameter ist, würde ich sie aus dem Zeichenfolgentyp ableiten:
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());
Denn dann kannst du einfach schreiben:
ConsoleMenu menu{L"Wide menu", L"invalid", L"> ", L". ", {/* choices */}};