c++ - 创建一个使用模板创建类或子类对象的函数

标签 c++ oop templates polymorphism generic-programming

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

#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管理 Menu以及一些其他对象(其他类型)。为了节省不需要创建的菜单的内存,我目前有 Menu此类的属性为 std::unique_ptr<Menu>当用户访问给定的菜单时,它会被重新分配(例如,商店游戏中的菜单,或餐厅的菜单,或书店的选择列表等)。 ConsoleUI类管理实现这些特定菜单。

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

#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_menu在这些功能中。 const应删除(或 m_menu 应设为 mutable )。

关于c++ - 创建一个使用模板创建类或子类对象的函数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59242148/

相关文章:

javascript - jQuery 模板 - 如何获取用户编辑输入的值

templates - 在 Backbone.js 中填充嵌套下划线模板

c++ - C++中的赋值运算符删除内存

c++ - 尝试学习 boost::intrusive Q2

java - java中错误和未经检查的异常之间的区别?

java - 用于在不使用控制逻辑的情况下执行算术运算的通用 Java 代码

c++ - std::array 的读取范围

c++ - 这段代码是否定义了通过基类指针删除派生类的行为?

c++ - 以下 g++ 类构造中的错误是什么?

oop - 为什么观察者设计模式通常被描绘成一对多?