Creador de menú de consola genérico solo de encabezado
Hace algún tiempo, escribí esta respuesta a una pregunta sobre la creación de un menú de línea de comandos. Me referí a él recientemente y noté algunas cosas que quería mejorar.
Propósito
Al igual que con la versión original, el propósito es tener una clase que simplifique la construcción y el uso de un menú de línea de comandos (consola).
Las mejoras que hice son:
- permitir
std::string
ostd::wstring
indicaciones y respuestas - permitir al usuario separar los selectores de las descripciones
- mover todo a un módulo de solo encabezado
- permitir la creación de
const
menús - ordenar por selectores
Preguntas
Algunas cosas sobre las que tenía preguntas son:
- nombres de parámetros de plantilla: ¿podrían mejorarse?
- uso de
default_in
ydefault_out
- ¿sería mejor inferir valores predeterminados del tipo de cadena? - elección de
std::function<void()>
la operación para cada elección - uso de
std::pair
vs objeto personalizado - ¿Debería envolver todo esto en un espacio de nombres?
- ¿Falta alguna funcionalidad?
- ¿Hay alguna forma de hacer una
constexpr
versión?
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();
}
}
Esto muestra el uso del programa y varias formas diferentes de crear funciones de menú. Dependiendo de la configuración de su consola y compilador, la oración en chino puede o no mostrarse correctamente. La siguiente es una versión de cadena ancha.
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();
}
}
Respuestas
Respuestas a tus preguntas
nombres de parámetros de plantilla: ¿podrían mejorarse?
Sobre todo es que son inconsistentes. Empiece a escribir los nombres con mayúscula y añádalos a todos con el sufijo Type
o no. Yo sugiero:
str
->Str
intype
->IStream
(solo para que quede claro que esperamos algo comostd::istream
aquí)outtype
->OStream
uso de default_in y default_out: ¿sería mejor inferir valores predeterminados del tipo de cadena?
Sí, ver más abajo.
elección de
std::function<void()>
la operación para cada elección
std::function<>
Aquí necesita almacenar las funciones para cada opción en el mapa. La única pregunta es si void()
es el tipo correcto para la función. Si quisiera operator()()
tomar parámetros y / o devolver un valor, también tendría que cambiar el tipo de función.
uso de std :: pair vs objeto personalizado
Personalmente, creo que está bien std::pair
.
¿Debería envolver todo esto en un espacio de nombres?
Si es justo class ConsoleMenu
, no creo que sea una mejora ponerlo en un espacio de nombres. Sin embargo, pondría default_in
y default_out
en un espacio de nombres, ya que esos nombres son bastante genéricos y no desea que contaminen el espacio de nombres global.
¿Falta alguna funcionalidad?
No lo sé, si esto es todo lo que necesitas, entonces está completo. Si necesita algo más de él, no lo es.
¿Hay alguna manera de hacer una versión constexpr?
Sí, asegurándose de que satisfaga los requisitos de LiteralType . Esto también significa que todas las variables miembro deben ser LiteralTypes válidos y eso evita el uso de std::string
o std::map
. Puede usar const char *
y en su std::array
lugar.
Pase el flujo de entrada y salida por valor
La construcción que tiene en la que pasa un tipo de flujo como parámetro de plantilla y luego hace que deduzca un flujo concreto de eso es muy extraño, inflexible y requiere más escritura de la necesaria. Simplemente agregue el flujo de entrada y salida como parámetros al constructor:
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}
Si desea que la entrada y la salida estándar sean un parámetro predeterminado, lo deduciría del tipo de cadena:
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 entonces puedes simplemente escribir:
ConsoleMenu menu{L"Wide menu", L"invalid", L"> ", L". ", {/* choices */}};