创建一个函数,使用模板创建 class 或子对象 class 的对象

Making a function that creates an object of a class OR child class using templates

我有一个名为 Menu 的父 class,它负责以格式化的方式将其属性显示到控制台。我还有这个 Menu class 的一些子 classes,它们可以以不同的方式显示附加信息或相同信息。这是一些示例代码:

#include <iostream>
#include <string>
#include <vector>

// Using just to make below code shorter for SO
using std::cout, std::string, std::vector;


class Menu
{
protected:
    string m_title;
    vector<string> m_options;
    string m_prompt;

public:
    Menu(string title, vector<string> options, string prompt = "Enter: ") :
        m_title(title), m_options(options), m_prompt(prompt)
    {}

    /**
     Displays the members of this object in a formatted way
    */
    virtual void run() const
    {
        // Display title
        cout << m_title << '\n';

        // Make a dashed underline
        for (const auto& ch : m_title)
        {
            cout << '-';
        }
        cout << "\n\n";

        // Display options
        for (int i = 0; i < m_options.size(); i++)
        {
            cout << '(' << (i + 1) << ") " << m_options.at(i) << '\n';
        }
        cout << '\n';

        // Display prompt
        cout << m_prompt << std::flush;
    }
};

/**
 Subclass of menu; allows for an "info" line below the title underline that
 gives instruction to the user
*/
class DescMenu : public Menu
{
private:
    string m_info;

public:
    DescMenu(string title, string info, vector<string> options, 
        string prompt = "Enter: ") : Menu(title, options, prompt),
        m_info(info)
    {}

    void run() const override
    {
        cout << m_title << '\n';
        for (const auto& ch : m_title)
        {
            cout << '-';
        }

        // Display extra info
        cout << '\n' << m_info << '\n';

        for (int i = 0; i < m_options.size(); i++)
        {
            cout << '(' << (i + 1) << ") " << m_options.at(i) << '\n';
        }
        cout << '\n';

        cout << m_prompt << std::flush;
    }
};

/**
 A trivial subclass of menu; does not display the dashed underline under title
*/
class NoDashMenu : public Menu
{
public:
    NoDashMenu(string title, vector<string> options, 
        string prompt = "Enter: ") : Menu(title, options, prompt)
    {}

    void run() const override
    {
        cout << m_title << "\n\n";

        for (int i = 0; i < m_options.size(); i++)
        {
            cout << '(' << (i + 1) << ") " << m_options.at(i) << '\n';
        }
        cout << '\n';

        cout << m_prompt << std::flush;
    }
};

我还有一个名为 ConsoleUI 的 class,它管理 Menu 以及一些其他对象(其他类型)。为了节省不需要创建的菜单的内存,我目前将此 class 的 Menu 属性作为 std::unique_ptr<Menu> 在用户访问给定菜单时重新分配(例如,商店游戏中的菜单、餐厅的菜单、书店的选择列表等)。 ConsoleUI class 管理实现这些特定菜单。

为了使这个更容易处理,而不是每次我想去一个新菜单时都输入“m_menu = std::make_unique<DescMenu>(DescMenu("Title", { "option1", "option2", "option3" }, "Prompt: "));”,我想在 class 中创建一个私有方法可以为需要创建的对象获取所需数量的参数,然后根据参数构造的对象将 m_menu 重新分配给 Menu 对象(或其子对象之一)。我也不想为每个对象类型的每个构造函数指定重载。对于概念示例,这里是一些示例代码及其构建错误(最后注释):

#include <memory> /* This is line 108 and comes after the code above (for build log reference) */

class ConsoleUI
{
private:
    using Menu_ptr = std::unique_ptr<Menu>;

    Menu_ptr m_menu;
    string   m_other1;
    unsigned m_other2;
    double   m_other3;

    template <typename MenuTy, typename... ConArgs>
    /**
     Makes a 'Menu' object or child-class object with the given parameters

     @returns A reference to the newly created menu
    */
    const Menu& reassign_menu(ConArgs... constructor_args)
    {
        m_menu = std::make_unique<MenuTy>(MenuTy(constructor_args...));
        return *m_menu;
    }

public:
    /* ... Constructor Here ... */

    void ShopMenu() const
    {
        //
        // IntelliSense detects the following error for the below code:
        // ------------------------------------------------------------
        //    no instance of function template "ConsoleUI::reassign_menu" 
        //    matches the argument list -- argument types are: (const char [5], 
        //    const char [33], {...})
        //
        reassign_menu<DescMenu>(
            "Shop", 
            "Select something you want to buy", 
            { "Cereal", "Bowl", "Knife", "Gun", "Steak" }
        ).run();

        /* ... Collect input, etc ... */
    }

    void BookStoreMenu() const
    {
        //
        // Same IntelliSense error below...
        //
        reassign_menu<NoDashMenu>(
            "Book Store",
            { "Harry Potter", "Narnia", "Check the back ;)" }
        ).run();

        /* ... Do more stuff ... */
    }
};


//
// g++ build log (autogenerated via Code Runner on VS Code):
//    
//    PS C:\Dev\C++\Others\DevBox\Testing> cd "c:\Dev\C++\Others\DevBox\Testing\src\" ; if ($?) { g++ -std=c++17 TestScript.cpp -o TestScript } ; if ($?) { .\TestScript }
//    TestScript.cpp: In member function 'void ConsoleUI::ShopMenu() const':
//    TestScript.cpp:148:9: error: no matching function for call to 'ConsoleUI::reassign_menu<DescMenu>(const char [5], const char [33], <brace-enclosed initializer list>) const'
//             ).run();
//             ^
//    TestScript.cpp:126:17: note: candidate: 'const Menu& ConsoleUI::reassign_menu(ConArgs ...) [with MenuTy = DescMenu; ConArgs = {}]'
//         const Menu& reassign_menu(ConArgs... constructor_args)
//                     ^~~~~~~~~~~~~
//    TestScript.cpp:126:17: note:   candidate expects 0 arguments, 3 provided
//    TestScript.cpp: In member function 'void ConsoleUI::BookStoreMenu() const':
//    TestScript.cpp:161:9: error: no matching function for call to 'ConsoleUI::reassign_menu<NoDashMenu>(const char [11], <brace-enclosed initializer list>) const'
//             ).run();
//             ^
//    TestScript.cpp:126:17: note: candidate: 'const Menu& ConsoleUI::reassign_menu(ConArgs ...) [with MenuTy = NoDashMenu; ConArgs = {}]'
//         const Menu& reassign_menu(ConArgs... constructor_args)
//                     ^~~~~~~~~~~~~
//    TestScript.cpp:126:17: note:   candidate expects 0 arguments, 2 provided
//

显然这是行不通的,所以我很好奇是否有办法做到这一点。此外,如果有人有更好的想法来管理 ConsoleUI 中的菜单系统,我会喜欢在评论或答案中提出建议,这些建议也提供了一种方法来完成我最初提出的问题(或解释为什么这样做不可能)。

这里的问题是 braced-init-list {...},它不适用于类型推导。

一个可能的解决方案是明确说明:

reassign_menu<DescMenu>(
    "Shop", "Select something you want to buy", 
    std::vector<std::string>{"Cereal", "Bowl", "Knife", "Gun", "Steak"}
).run();

另一种选择是更改构造函数中参数的顺序,使 std::initializer_list 成为第一个参数:

DescMenu(std::vector<std::string> options, std::string title, std::string info,
         std::string prompt = "Enter: ") : ...

template<typename MenuTy, typename... Args>
const Menu& reassign_menu(std::initializer_list<std::string> il, Args... args) {
    m_menu = std::make_unique<MenuTy>(il, args...);
    return *m_menu;
}

...

reassign_menu<DescMenu>({"Cereal", "Bowl", "Knife", "Gun", "Steak"},
    "Shop", "Select something you want to buy").run();

后一种方法在标准库中被广泛使用。例如,参见 std::optional<T>::emplace.

的声明

补充说明:

  1. 如果你想对 std::initializer_list 类型通用(使其成为 T),请注意 std::vector<std::string> 不能直接从 std::initializer_list<const char*> 构造。您可以使用带有一对迭代器的构造函数:std::vector<std::string>(il.begin(), il.end()).
  2. std::make_unique<MenuTy>(MenuTy(args...));可以简化为std::make_unique<MenuTy>(args...);.
  3. ShopMenu()BookStoreMenu()标记为const。您不能在这些函数中修改 m_menuconst 应删除(或 m_menu 应设为 mutable)。