Créateur de menu de console générique uniquement en-tête

Dec 20 2020

Il y a quelque temps, j'ai écrit cette réponse à une question sur la création d'un menu en ligne de commande. J'y ai fait allusion récemment et j'ai remarqué certaines choses que je voulais améliorer.

Objectif

Comme pour la version originale, le but est d'avoir une classe qui simplifie la construction et l'utilisation d'un menu en ligne de commande (console).

Les améliorations que j'ai apportées sont:

  1. permettre soit std::stringou std::wstringinvites et réponses
  2. permettre à l'utilisateur de séparer les sélecteurs des descriptions
  3. tout déplacer dans un module d'en-tête uniquement
  4. permettre la création de constmenus
  5. trier par sélecteurs

Des questions

Certaines choses sur lesquelles j'avais des questions sont:

  1. noms de paramètres de modèle - pourraient-ils être améliorés?
  2. utilisation de default_inet default_out- serait-il préférable de déduire les valeurs par défaut du type chaîne?
  3. choix de std::function<void()>l'opération pour chaque choix
  4. utilisation de std::pairvs objet personnalisé
  5. dois-je envelopper tout cela dans un espace de noms?
  6. une fonctionnalité manque-t-elle?
  7. existe-t-il un moyen de créer une constexprversion?

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

Cela montre l'utilisation du programme et plusieurs façons différentes de créer des fonctions de menu. Selon les paramètres de votre console et de votre compilateur, la phrase chinoise peut ou non s'afficher correctement. Vient ensuite une version à chaîne large.

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

Réponses

3 G.Sliepen Dec 20 2020 at 19:16

Réponses à vos questions

noms de paramètres de modèle - pourraient-ils être améliorés?

La plupart du temps, c'est qu'ils sont incohérents. Commencez les noms de type par une majuscule et ajoutez-les tous Typeou non. Je suggère:

  • str -> Str
  • intype-> IStream(juste pour être clair, nous nous attendons à quelque chose comme std::istreamici)
  • outtype -> OStream

utilisation de default_in et default_out - serait-il préférable de déduire les valeurs par défaut à partir du type de chaîne?

Oui, voir ci-dessous.

choix de std::function<void()>l'opération pour chaque choix

Vous devez std::function<>ici stocker les fonctions pour chaque choix dans la carte. La seule question est de savoir si void()est le bon type pour la fonction. Si vous vouliez operator()()prendre des paramètres et / ou renvoyer une valeur, vous devrez également changer le type de la fonction.

utilisation de std :: pair vs objet personnalisé

Personnellement, je pense que ça va std::pair.

dois-je envelopper tout cela dans un espace de noms?

Si c'est juste class ConsoleMenu, je ne pense pas que ce serait une amélioration de le mettre dans un espace de noms. Cependant, je mettrais default_inet default_outdans un espace de noms, car ces noms sont assez génériques et vous ne voulez pas qu'ils polluent l'espace de noms global.

une fonctionnalité manque-t-elle?

Je ne sais pas, si c'est tout ce dont vous avez besoin, alors c'est complet. Si vous avez besoin de quelque chose d'autre, ce n'est pas le cas.

existe-t-il un moyen de créer une version constexpr?

Oui, en vous assurant qu'il répond aux exigences de LiteralType . Cela signifie également que toutes les variables membres doivent être des LiteralTypes valides, ce qui empêche l'utilisation de std::stringou std::map. Vous pouvez utiliser const char *et à la std::arrayplace.

Passer le flux d'entrée et de sortie par valeur

La construction que vous avez dans laquelle vous passez un type de flux en tant que paramètre de modèle, puis faites en déduire un flux concret est très étrange, inflexible et nécessite plus de frappe que nécessaire. Ajoutez simplement le flux d'entrée et de sortie en tant que paramètres au constructeur:

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

Comparer:

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

Contre:

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

Si vous voulez que l'entrée et la sortie standard soient un paramètre par défaut, je le déduire du type de chaîne:

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

Parce qu'alors, vous pouvez simplement écrire:

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