Kreator menu konsoli obsługujący tylko nagłówki

Dec 20 2020

Jakiś czas temu napisałem tę odpowiedź na pytanie dotyczące tworzenia menu wiersza poleceń. Odniosłem się niedawno do tego i zauważyłem kilka rzeczy, które chciałem poprawić.

Cel, powód

Podobnie jak w przypadku wersji oryginalnej, celem jest posiadanie klasy, która upraszcza konstrukcję i używanie menu wiersza poleceń (konsoli).

Wprowadzone przeze mnie ulepszenia to:

  1. pozwalają albo std::stringczy std::wstringpodpowiedzi i odpowiedzi
  2. pozwalają użytkownikowi na oddzielenie selektorów od opisów
  3. przenieś wszystko do modułu tylko z nagłówkiem
  4. pozwalają na tworzenie constmenu
  5. sortuj według selektorów

pytania

Niektóre rzeczy, o które miałem pytania, to:

  1. nazwy parametrów szablonów - czy można je ulepszyć?
  2. użycie default_ini default_out- czy lepiej byłoby wywnioskować wartości domyślne z typu łańcucha?
  3. wybór std::function<void()>jako operacja dla każdego wyboru
  4. użycie std::paira obiekt niestandardowy
  5. czy powinienem to wszystko opakować w przestrzeń nazw?
  6. brakuje jakiejś funkcjonalności?
  7. czy jest sposób na wykonanie constexprwersji?

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();
    }
}

Pokazuje użycie programu i kilka różnych sposobów tworzenia funkcji menu. W zależności od ustawień konsoli i kompilatora chińskie zdanie może być wyświetlane poprawnie lub nie. Dalej jest wersja z szerokimi strunami.

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();
    }
}

Odpowiedzi

3 G.Sliepen Dec 20 2020 at 19:16

Odpowiedzi na Twoje pytania

nazwy parametrów szablonów - czy można je ulepszyć?

Głównie chodzi o to, że są niespójne. Nazwy typów rozpoczynaj od dużej litery i dodawaj do nich sufiks Typelub nie. Sugeruję:

  • str -> Str
  • intype-> IStream(żeby było jasne, spodziewamy się czegoś takiego jak std::istreamtutaj)
  • outtype -> OStream

użycie default_in i default_out - czy lepiej byłoby wywnioskować wartości domyślne z typu łańcucha?

Tak, patrz poniżej.

wybór std::function<void()>jako operacja dla każdego wyboru

Musisz std::function<>tutaj zapisać funkcje dla każdego wyboru na mapie. Jedyne pytanie brzmi, czy void()jest to właściwy typ funkcji. Jeśli chciałbyś operator()()pobrać parametry i / lub zwrócić wartość, musiałbyś również zmienić typ funkcji.

użycie std :: pair a obiekt niestandardowy

Osobiście uważam, że to w porządku std::pair.

czy powinienem to wszystko opakować w przestrzeń nazw?

Jeśli to jest sprawiedliwe class ConsoleMenu, nie sądzę, by było to jakiekolwiek ulepszenie, aby umieścić go w przestrzeni nazw. Jednak umieściłbym default_ini default_outw przestrzeni nazw, ponieważ te nazwy są dość ogólne i nie chcesz, aby zanieczyszczały globalną przestrzeń nazw.

brakuje jakiejś funkcjonalności?

Nie wiem, czy to wszystko, czego potrzebujesz, to jest kompletne. Jeśli potrzebujesz czegoś innego, to nie jest.

czy istnieje sposób na stworzenie wersji constexpr?

Tak, upewniając się, że spełnia wymagania LiteralType . Oznacza to również, że wszystkie zmienne składowe muszą być poprawnymi LiteralTypes, co uniemożliwia użycie std::stringlub std::map. Zamiast tego możesz użyć const char *i std::array.

Przekaż strumień wejściowy i wyjściowy według wartości

Konstrukcja, w której przekazujesz typ strumienia jako parametr szablonu, a następnie wyprowadzasz z tego konkretny strumień, jest bardzo dziwna, nieelastyczna i wymaga więcej wpisywania niż to konieczne. Po prostu dodaj strumień wejściowy i wyjściowy jako parametry do konstruktora:

template <class str, class intype, class outtype>
class ConsoleMenu {
public:
    ConsoleMenu(const str& message,
        ...,
        intype &in,
        outtype &out);

Porównać:

ConsoleMenu<std::wstring, std::wistream, std::wostream> menu{...}

Przeciw:

ConsoleMenu<std::wstring> menu{..., std::wcin, std::wcout}

Jeśli chcesz, aby standardowe wejście i wyjście było parametrem domyślnym, wywnioskowałbym to z typu ciągu:

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());

Bo wtedy możesz po prostu napisać:

ConsoleMenu menu{L"Wide menu", L"invalid", L"> ", L". ", {/* choices */}};