Creador de menú de consola genérico solo de encabezado

Dec 20 2020

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:

  1. permitir std::stringo std::wstringindicaciones y respuestas
  2. permitir al usuario separar los selectores de las descripciones
  3. mover todo a un módulo de solo encabezado
  4. permitir la creación de constmenús
  5. ordenar por selectores

Preguntas

Algunas cosas sobre las que tenía preguntas son:

  1. nombres de parámetros de plantilla: ¿podrían mejorarse?
  2. uso de default_iny default_out- ¿sería mejor inferir valores predeterminados del tipo de cadena?
  3. elección de std::function<void()>la operación para cada elección
  4. uso de std::pairvs objeto personalizado
  5. ¿Debería envolver todo esto en un espacio de nombres?
  6. ¿Falta alguna funcionalidad?
  7. ¿Hay alguna forma de hacer una constexprversió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 &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();
    }
}

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

3 G.Sliepen Dec 20 2020 at 19:16

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 Typeo no. Yo sugiero:

  • str -> Str
  • intype-> IStream(solo para que quede claro que esperamos algo como std::istreamaquí)
  • 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_iny default_outen 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::stringo std::map. Puede usar const char *y en su std::arraylugar.

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 */}};