Generischer Konsolenmenühersteller nur für Header

Dec 20 2020

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:

  1. Erlaube entweder std::stringoder std::wstringAufforderungen und Antworten
  2. Ermöglichen Sie dem Benutzer, Selektoren von Beschreibungen zu trennen
  3. Verschieben Sie alles in ein Nur-Header-Modul
  4. Ermöglichen die Erstellung von constMenüs
  5. Sortieren nach Selektoren

Fragen

Einige Dinge, zu denen ich Fragen hatte, sind:

  1. Vorlagenparameternamen - könnten sie verbessert werden?
  2. Verwendung von default_inund default_out- wäre es besser, Standardeinstellungen aus dem Zeichenfolgentyp abzuleiten?
  3. Wahl std::function<void()>als Operation für jede Wahl
  4. Verwendung von std::pairvs. benutzerdefinierten Objekt
  5. soll ich das alles in einen Namespace einschließen?
  6. Fehlt eine Funktionalität?
  7. Gibt es eine Möglichkeit, eine constexprVersion 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 &in;
    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

3 G.Sliepen Dec 20 2020 at 19:16

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 Typeoder nicht. Ich schlage vor:

  • str -> Str
  • intype-> IStream(nur um klar zu sein, dass wir so etwas wie std::istreamhier 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_inund default_outin 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::stringoder std::map. Sie können const char *und std::arraystattdessen 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 */}};