Конструктор универсального меню консоли только для заголовков

Dec 20 2020

Некоторое время назад я написал этот ответ на вопрос о создании меню командной строки. Я недавно упомянул об этом и заметил несколько вещей, которые хотел улучшить.

Цель

Как и в исходной версии, целью является создание класса, который упрощает создание и использование меню командной строки (консоли).

Я сделал следующие улучшения:

  1. разрешить либо подсказки, std::stringлибо std::wstringответы
  2. позволяют пользователю отделять селекторы от описаний
  3. переместить все в модуль только для заголовков
  4. разрешить создание constменю
  5. сортировать по селекторам

Вопросы

У меня возникли вопросы:

  1. имена параметров шаблона - можно ли их улучшить?
  2. использование default_inи default_out- не лучше ли вывести значения по умолчанию из строкового типа?
  3. выбор в std::function<void()>качестве операции для каждого выбора
  4. использование по std::pairсравнению с настраиваемым объектом
  5. я должен обернуть все это в пространство имен?
  6. отсутствует какая-либо функциональность?
  7. есть способ сделать constexprверсию?

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

Здесь показано использование программы и несколько различных способов создания функций меню. В зависимости от настроек вашей консоли и компилятора предложение на китайском языке может отображаться или отображаться неправильно. Далее идет версия с широкой строкой.

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

Ответы

3 G.Sliepen Dec 20 2020 at 19:16

Ответы на ваши вопросы

имена параметров шаблона - можно ли их улучшить?

В основном они непоследовательны. Начинайте имена с заглавной буквы и либо добавляйте к ним суффикс, Typeлибо нет. Я предлагаю:

  • str -> Str
  • intype-> IStream(просто чтобы прояснить, мы действительно ожидаем чего-то вроде std::istreamздесь)
  • outtype -> OStream

использование default_in и default_out - не лучше ли вывести значения по умолчанию из строкового типа?

Да, смотрите ниже.

выбор в std::function<void()>качестве операции для каждого выбора

Здесь вам нужно std::function<>сохранить функции для каждого выбора на карте. Вопрос только в том, подходит ли void()этот тип функции. Если вы хотите operator()()принимать параметры и / или возвращать значение, вам также придется изменить тип функции.

использование std :: pair против настраиваемого объекта

Я лично считаю, что это нормально std::pair.

я должен обернуть все это в пространство имен?

Если это справедливо class ConsoleMenu, я не думаю, что было бы лучше поместить его в пространство имен. Однако я бы поместил default_inи default_outв пространство имен, так как эти имена довольно общие, и вы не хотите, чтобы они загрязняли глобальное пространство имен.

отсутствует какая-либо функциональность?

Не знаю, если это все, что вам нужно, значит, все готово. Если вам нужно что-то еще от него, это не так.

есть ли способ сделать версию constexpr?

Да, убедившись, что он удовлетворяет требованиям LiteralType . Это также означает, что все переменные-члены должны быть допустимыми типами LiteralTypes, что предотвращает использование std::stringили std::map. Вместо этого можно использовать const char *и std::array.

Передайте поток ввода и вывода по значению

У вас есть конструкция, в которой вы передаете тип потока в качестве параметра шаблона, а затем заставляете его выводить из него конкретный поток, очень странно, негибко и требует большего набора текста, чем необходимо. Просто добавьте поток ввода и вывода в качестве параметров конструктору:

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

Сравните:

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

Против:

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

Если вы хотите, чтобы стандартный ввод и вывод был параметром по умолчанию, я бы вывести его из строкового типа:

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

Потому что тогда вы можете просто написать:

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