Criador de menu de console genérico apenas de cabeçalho

Dec 20 2020

Há algum tempo, respondi a uma pergunta sobre a criação de um menu de linha de comando. Eu me referi a ele recentemente e percebi algumas coisas que queria melhorar.

Objetivo

Como na versão original, o objetivo é ter uma classe que simplifique a construção e o uso de um menu de linha de comando (console).

As melhorias que fiz são:

  1. permitir std::stringou std::wstringprompts e respostas
  2. permite ao usuário separar seletores de descrições
  3. mover tudo para um módulo apenas de cabeçalho
  4. permitir a criação de constmenus
  5. classificar por seletores

Questões

Algumas coisas sobre as quais tive dúvidas são:

  1. nomes de parâmetros de modelo - eles poderiam ser melhorados?
  2. uso de default_ine default_out- seria melhor inferir padrões a partir do tipo de string?
  3. escolha de std::function<void()>como a operação para cada escolha
  4. uso de std::pairobjeto personalizado vs. personalizado
  5. devo envolver tudo isso em um namespace?
  6. está faltando alguma funcionalidade?
  7. existe uma maneira de fazer uma constexprversão?

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

Isso mostra o uso do programa e várias maneiras diferentes de criar funções de menu. Dependendo das configurações do console e do compilador, a frase em chinês pode ou não ser exibida corretamente. A seguir está uma versão de string ampla.

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

Respostas

3 G.Sliepen Dec 20 2020 at 19:16

Respostas às suas perguntas

nomes de parâmetros de modelo - eles poderiam ser melhorados?

Principalmente porque eles são inconsistentes. Comece os nomes de tipo com maiúscula e coloque todos eles como sufixo Typeou não. Eu sugiro:

  • str -> Str
  • intype-> IStream(só para ficar claro que esperamos algo como std::istreamaqui)
  • outtype -> OStream

uso de default_in e default_out - seria melhor inferir padrões a partir do tipo de string?

Sim, veja abaixo.

escolha de std::function<void()>como a operação para cada escolha

Você precisa std::function<>aqui para armazenar as funções para cada escolha no mapa. A única questão é se void()é o tipo certo para a função. Se você quiser operator()()obter parâmetros e / ou retornar um valor, terá que alterar o tipo da função também.

uso de std :: pair vs. objeto personalizado

Eu pessoalmente acho que está tudo bem com std::pair.

devo envolver tudo isso em um namespace?

Se for justo class ConsoleMenu, não acho que seria uma melhoria colocá-lo em um namespace. No entanto, eu colocaria default_ine default_outem um namespace, pois esses nomes são bastante genéricos e você não quer que eles poluam o namespace global.

está faltando alguma funcionalidade?

Não sei, se isso é tudo que você precisa, então está completo. Se você precisar de algo mais, não é.

existe uma maneira de fazer uma versão constexpr?

Sim, certificando-se de que satisfaz os requisitos de LiteralType . Isso também significa que todas as variáveis ​​de membro devem ser LiteralTypes válidos e isso impede o uso de std::stringou std::map. Você pode usar const char *e em std::arrayvez disso.

Passe o fluxo de entrada e saída por valor

A construção que você tem em que passa um tipo de fluxo como um parâmetro de modelo e, em seguida, deduz um fluxo concreto disso é muito estranho, inflexível e requer mais digitação do que o necessário. Basta adicionar o fluxo de entrada e saída como parâmetros para o construtor:

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

Comparar:

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

Versus:

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

Se você quiser que a entrada e a saída padrão sejam um parâmetro padrão, eu deduzo do tipo de string:

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

Porque então você pode simplesmente escrever:

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