Criador de menu de console genérico apenas de cabeçalho
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:
- permitir
std::string
oustd::wstring
prompts e respostas - permite ao usuário separar seletores de descrições
- mover tudo para um módulo apenas de cabeçalho
- permitir a criação de
const
menus - classificar por seletores
Questões
Algumas coisas sobre as quais tive dúvidas são:
- nomes de parâmetros de modelo - eles poderiam ser melhorados?
- uso de
default_in
edefault_out
- seria melhor inferir padrões a partir do tipo de string? - escolha de
std::function<void()>
como a operação para cada escolha - uso de
std::pair
objeto personalizado vs. personalizado - devo envolver tudo isso em um namespace?
- está faltando alguma funcionalidade?
- existe uma maneira de fazer uma
constexpr
versã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 ∈
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
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 Type
ou não. Eu sugiro:
str
->Str
intype
->IStream
(só para ficar claro que esperamos algo comostd::istream
aqui)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_in
e default_out
em 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::string
ou std::map
. Você pode usar const char *
e em std::array
vez 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 */}};