Reputation: 664
I have a parent class called Menu
, which is responsible for displaying its attributes in a formatted way to the console. I also have some child classes of this Menu
class which can display additional information or the same information in a different way. Here is some example code:
#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;
}
};
I also have a class called ConsoleUI
that manages a Menu
along with a few other objects (of other types). To save on memory for menus that don't need to be created, I currently have the Menu
attribute of this class as an std::unique_ptr<Menu>
that is reassigned when the user access a given menu (e.g. a menu in a game for a shop, or a menu at a restaurant, or a list of choices at a bookstore, etc.). The ConsoleUI
class manages implements these specific menus.
To make this easier to deal with, instead of typing "m_menu = std::make_unique<DescMenu>(DescMenu("Title", { "option1", "option2", "option3" }, "Prompt: "));
" everytime I want to go to a new menu, I'd like to make a private method inside the class that can take in the needed number of arguments for the object that needs to be created, then reassign m_menu
to the Menu
object (or one of its child objects) based on the object constructed from the parameters. I'd also like to not have to specify an overload for every single constructor of every single object type. For a conceptual example, here is some example code and its build errors (commented at the end):
#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
//
Obviously this isn't working, so I am curious if there is a way to do this. Also, if anyone has a better idea to manage the menu system in ConsoleUI
, I'd love suggestions in the comments or in an answer that also gives a way to do what I'm originally asking (or explains why it isn't possible).
Upvotes: 0
Views: 58
Reputation: 26302
The problem here is the braced-init-list {...}
, that doesn't work with type deduction.
One possible solution is to be explicit about it:
reassign_menu<DescMenu>(
"Shop", "Select something you want to buy",
std::vector<std::string>{"Cereal", "Bowl", "Knife", "Gun", "Steak"}
).run();
Another option is to change the order of parameters in the constructors and make the std::initializer_list
the first parameter:
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();
The latter approach is widely used in the standard library. For example, see the declaration of std::optional<T>::emplace
.
Additional notes:
std::initializer_list
type (make it T
), note that std::vector<std::string>
cannot be constructed from std::initializer_list<const char*>
directly. You can use the constructor that takes a pair of iterators: std::vector<std::string>(il.begin(), il.end())
.std::make_unique<MenuTy>(MenuTy(args...));
can be simplified to std::make_unique<MenuTy>(args...);
.ShopMenu()
and BookStoreMenu()
are marked as const
. You can't modify m_menu
in these functions. const
should be removed (or m_menu
should be made mutable
).Upvotes: 1