Pembuat menu konsol generik hanya header

Dec 20 2020

Beberapa waktu yang lalu, saya menulis jawaban ini untuk pertanyaan tentang membuat menu baris perintah. Saya merujuknya baru - baru ini dan memperhatikan beberapa hal yang ingin saya perbaiki.

Tujuan

Seperti versi aslinya, tujuannya adalah untuk memiliki kelas yang menyederhanakan konstruksi dan penggunaan menu baris perintah (konsol).

Perbaikan yang saya lakukan adalah:

  1. mengizinkan salah satu std::stringatau std::wstringpetunjuk dan jawaban
  2. memungkinkan pengguna untuk memisahkan pemilih dari deskripsi
  3. pindahkan semuanya ke dalam modul khusus header
  4. memungkinkan pembuatan constmenu
  5. urutkan berdasarkan pemilih

Pertanyaan

Beberapa hal yang saya pertanyakan adalah:

  1. nama parameter template - apakah bisa ditingkatkan?
  2. penggunaan default_indan default_out- apakah lebih baik menyimpulkan default dari tipe string?
  3. pilihan std::function<void()>sebagai operasi untuk setiap pilihan
  4. penggunaan std::pairvs. objek khusus
  5. haruskah saya membungkus semua ini dalam namespace?
  6. apakah ada fungsi yang hilang?
  7. apakah ada cara untuk membuat constexprversi?

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

Ini menunjukkan penggunaan program dan beberapa cara berbeda untuk membuat fungsi menu. Bergantung pada konsol dan pengaturan compiler Anda, kalimat bahasa Mandarin mungkin ditampilkan atau tidak dengan benar. Berikutnya adalah versi string lebar.

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

Jawaban

3 G.Sliepen Dec 20 2020 at 19:16

Jawaban atas pertanyaan Anda

nama parameter template - apakah bisa ditingkatkan?

Sebagian besar karena mereka tidak konsisten. Mulai ketik nama dengan huruf besar, dan pilih akhiran semuanya dengan Typeatau tidak. Saya menyarankan:

  • str -> Str
  • intype-> IStream(hanya untuk memperjelas bahwa kami mengharapkan sesuatu seperti di std::istreamsini)
  • outtype -> OStream

penggunaan default_in dan default_out - apakah lebih baik menyimpulkan default dari tipe string?

Ya, lihat di bawah.

pilihan std::function<void()>sebagai operasi untuk setiap pilihan

Di std::function<>sini Anda perlu menyimpan fungsi untuk setiap pilihan di peta. Satu-satunya pertanyaan adalah apakah void()tipe yang tepat untuk fungsi tersebut. Jika Anda ingin operator()()mengambil parameter dan / atau mengembalikan nilai, maka Anda harus mengubah jenis fungsinya juga.

penggunaan std :: pair vs. objek khusus

Saya pribadi berpikir tidak masalah std::pair.

haruskah saya membungkus semua ini dalam namespace?

Jika hanya class ConsoleMenu, saya rasa tidak akan ada perbaikan untuk meletakkannya di namespace. Namun, saya akan meletakkan default_indan default_outdi namespace, karena nama-nama itu cukup umum, dan Anda tidak ingin mereka mencemari namespace global.

apakah ada fungsi yang hilang?

Saya tidak tahu, jika hanya ini yang Anda butuhkan maka itu lengkap. Jika Anda membutuhkan sesuatu yang lain darinya, itu tidak.

apakah ada cara untuk membuat versi constexpr?

Ya, dengan memastikannya memenuhi persyaratan LiteralType . Ini juga berarti bahwa semua variabel anggota harus merupakan LiteralTypes yang valid, dan mencegah penggunaan std::stringatau std::map. Anda dapat menggunakan const char *dan std::arraysebagai gantinya.

Meneruskan aliran input dan output berdasarkan nilai

Konstruksi yang Anda miliki di mana Anda meneruskan jenis aliran sebagai parameter templat, dan kemudian menyimpulkan aliran konkret darinya sangat aneh, tidak fleksibel, dan membutuhkan lebih banyak pengetikan daripada yang diperlukan. Cukup tambahkan aliran input dan output sebagai parameter ke konstruktor:

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

Membandingkan:

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

Melawan:

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

Jika Anda ingin input dan output standar menjadi parameter default, maka saya akan menyimpulkannya dari tipe 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());

Karena dengan begitu Anda bisa menulis:

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