问题
I have a Menu<T>
class, whose options are items of type T, and it may have submenus of type Menu<T>
(with no limit to the depth of nested submenus).
template <typename T>
class Menu {
private:
class Option {
const std::string name;
const T item;
Menu<T>* submenu;
Option* next = nullptr;
friend class Menu<T>;
Option (const std::string& itemName, const T& t, Menu<T>* menu = nullptr) : name(itemName), item(t), submenu(menu) {}
~Option() {if (submenu) delete submenu;}
inline T choose() const;
inline void print (int n) const;
};
Option* first = nullptr; // first option in the menu
Menu<T>* parent = nullptr;
Option* parentOption = nullptr;
enum ChoosingType {Normal, Remove};
public:
Menu() = default;
Menu (const Menu<T>&);
Menu& operator = (const Menu<T>&);
~Menu();
inline void insert (const std::string& itemName, const T& t, Menu<T>* submenu = nullptr, int pos = END_OF_MENU);
T choose() const {return chosenItem().first;}
inline int print() const;
private:
inline std::pair<T, int> chosenItem (ChoosingType = Normal) const;
inline Option* deepCopy (const Option*);
};
And I've tested that it works correctly, but my Menu<T>
class above does not support submenus whose items are of a different type than T. This extra feature would be very handy, if say, the main menu had Action
as the type for its options, and then one of the options is "Take out weapon", whose submenu would ideally like to have Weapon
as its options, but as it is right now, the submenu would again have to have Action
as its options.
My attempt to generalize with
template <typename T, typename U, typename... Rest>
class Menu { // Menu with T as its option types.
private:
class Option {
const std::string name;
const T item;
Menu<U, Rest...>* submenu; // Submenu with U as its option types.
Option* next = nullptr;
friend class Menu<T, U, Rest...>;
Option (const std::string& itemName, const T& t, Menu<U, Rest...>* menu = nullptr) : name(itemName), item(t), submenu(menu) {}
~Option() {if (submenu) delete submenu;}
inline T choose() const;
inline void print (int n) const;
};
Option* first = nullptr;
// ....
}
int main() {
Menu<int, std::string> menu; // Will not compile.
}
is not proper because Menu<int, std::string> menu;
whereby I'm trying to create a simple menu of int options with submenus of string options, won't even compile because then the submenus are of type Menu<std::string>
which does not match the class template. It also doesn't make sense because Menu<int, std::string>
is to return int from its choose()
function, but going into its submenu will then return a string. boost::variant needed here?
I just need someone to point out how to start. I know this may seem to belong to CodeReview, but there they only want to examine my code if it is already working, but here my attempt to generalize is nowhere near working (hasn't even started yet), so I need to appeal to the experts here on just how to start.
Update: Following gmbeard's suggestion, I got it working with the following simplified code (the real Menu classes will have linked list of options which the user will choose from through input). But there are drawbacks.
#include <iostream>
#include <string>
struct Visitor {
virtual void visit (int&) = 0;
virtual void visit (std::string&) = 0;
virtual void visit (char& c) = 0;
};
struct ChooseVisitor : Visitor {
std::pair<int, bool> chosenInt;
std::pair<std::string, bool> chosenString;
std::pair<char, bool> chosenCharacter;
virtual void visit (int& num) override {
chosenInt.first = num;
chosenInt.second = true;
}
virtual void visit (std::string& str) override {
chosenString.first = str;
chosenString.second = true;
}
virtual void visit (char& c) override {
chosenCharacter.first = c;
chosenCharacter.second = true;
}
};
template <typename...> struct Menu;
template <typename T>
struct Menu<T> {
struct Option {
T item;
void accept (ChooseVisitor& visitor) {visitor.visit(item);}
};
Option* option; // Assume only one option for simplicity here.
ChooseVisitor choose() const {
ChooseVisitor visitor;
option->accept(visitor);
return visitor;
}
void insert (const T& t) {option = new Option{t};}
};
// A specialization for the Menu instances that will have submenus.
template <typename T, typename... Rest>
struct Menu<T, Rest...> { // Menu with T as its options type.
struct Option {
T item;
Menu<Rest...>* submenu; // Submenu with the first type in Rest... as its options type.
void accept (ChooseVisitor& visitor) {visitor.visit(item);}
};
Option* option;
ChooseVisitor choose() const {
// In reality there will be user input, of course. The user might not choose to enter a submenu,
// but instead choose from among the options in the current menu.
ChooseVisitor visitor;
if (option->submenu)
return option->submenu->choose();
else
option->accept(visitor);
return visitor;
}
void insert (const T& t, Menu<Rest...>* submenu = nullptr) {option = new Option{t, submenu};}
};
int main() {
Menu<int, std::string, char> menu;
Menu<std::string, char> submenu;
Menu<char> subsubmenu;
subsubmenu.insert('t');
submenu.insert ("", &subsubmenu);
menu.insert (0, &submenu);
const ChooseVisitor visitor = menu.choose();
if (visitor.chosenInt.second)
std::cout << "You chose " << visitor.chosenInt.first << ".\n"; // Do whatever with it.
else if (visitor.chosenString.second)
std::cout << "You chose " << visitor.chosenString.first << ".\n"; // Do whatever with it.
else if (visitor.chosenCharacter.second)
std::cout << "You chose " << visitor.chosenCharacter.first << ".\n"; // Do whatever with it.
}
Output:
You chose t.
The biggest problem is that ChooseVisitor
needs to be constantly updated for all possible Menu option types (it could end up with literally hundreds of data members and overloads), not to mention the horrendous if-checks to get the desired returned item. But the chosen item needs to be stored, and not just used for the short term. I welcome ideas for improvement.
回答1:
One solution is to create some partial specializations of Menu
to unwrap the variadic parameter pack.
First, create the template class...
// Can be incomplete; only specialized versions will be instantiated...
template<typename... Args> class Menu;
Now, create a specialization for the end of the menu chain (no submenus)...
template<typename T>
class Menu<T>
{
public:
// No submenu in this specialization
using item_type = T;
std::vector<item_type> items;
...
};
Lastly, create a specialization for the Menu
instances that will have submenus...
template<typename Head, typename... Tail>
class Menu<Head, Tail...>
{
public:
Menu<Tail...> submenu;
using item_type = Head;
std::vector<item_type> items;
...
};
This is a simplified version of your class for brevity but the same principle still applies if you add a nested Option
class.
You can use a similar technique to recurse through the submenus by overloading a non-member function...
template<typename T>
void
print_menu(Menu<T> const& menu)
{
for(auto i : menu.items) {
std::cout << i << std::endl;
}
}
template<typename... T>
void
print_menu(Menu<T...> const& menu)
{
for(auto i : menu.items) {
std::cout << i << std::endl;
}
print_menu(menu.submenu);
}
int
main(int, char*[])
{
Menu<int, std::string> menu{};
menu.items.emplace_back(1);
menu.submenu.items.emplace_back("42");
print_menu(menu);
...
}
Update: A possible implementation of the choose()
functionality could use a visitor pattern. You would need to provide a type that overloads operator()
for each type contained in your menu (in this case, int
and std::string
) ...
struct ChooseVisitor
{
void operator()(std::string const& string_val) const
{ /* Do something when a string value is chosen */ }
void operator()(int int_val) const
{ /* Do something when an int value is chosen */ }
};
Similar to the print_menu
function, you could define a couple of choose_menu
function overloads ...
template<typename... Types, typename Visitor>
void
choose_menu(Menu<Types...> const& menu, Visitor const& visitor)
{
for(auto i : menu.items) {
if(/* is menu item chosen? */) {
visitor(i);
return;
}
}
choose_menu(menu.Submenu, visitor);
}
template<typename Type, typename Visitor>
void
choose_menu(Menu<Type> const& menu, Visitor const& visitor)
{
for(auto i : menu.items) {
if(/* is menu item chosen? */) {
visitor(i);
return;
}
}
}
This would be used like so ...
Menu<int, std::string> menu{};
...
choose_menu(menu, ChooseVisitor{});
It's a bit difficult to derive what you had in mind for your choose()
function but you should be able to adapt the above to suit most scenarios.
回答2:
I have a slight feeling things are being over engineered here.
Here's a version using Boost Variant, as you surmised.
I simplified a few things. In particular I'd favour an initializer list constructor, so you can simply construct the menu tree recursively like:
Menu menu = MenuT<int> {
{ "one", 1 },
{ "two", 2 },
{ "three", 3 },
{ "forty-two", 42,
Menu(MenuT<std::string> {
{"Life, The Universe And Everything", "LtUae"},
{"Dent", "Arthur",
Menu(MenuT<bool> {
{"yes", true},
{"no", false},
})
},
})
},
};
As you can see it mixes MenuT<int>
, MenuT<std::string>
and MenuT<bool>
at the different levels. You could visit this without much ado:
struct simple : boost::static_visitor<>
{
void operator()(Menu& m) const { boost::apply_visitor(*this, m); }
template <typename T> void operator()(MenuT<T>& m) const {
std::cout << "-----------\n";
for (auto& o : m.options) {
std::cout << "option '" << o.name << "':\t" << o.item << "\n";
if (o.submenu)
(*this)(*o.submenu);
}
}
};
Which prints
-----------
option 'one': 1
option 'two': 2
option 'three': 3
option 'forty-two': 42
-----------
option 'Life, The Universe And Everything': LtUae
option 'Dent': Arthur
-----------
option 'yes': true
option 'no': false
Live On Coliru
#include <string>
#include <vector>
#include <boost/optional.hpp>
#include <boost/variant.hpp>
#include <iostream>
template <typename> struct MenuT;
using Menu = boost::make_recursive_variant <
boost::recursive_wrapper<MenuT<int>>,
boost::recursive_wrapper<MenuT<std::string>>,
boost::recursive_wrapper<MenuT<bool>>
>::type;
template <typename T> struct MenuT {
struct Option {
std::string name;
T item;
boost::optional<Menu> submenu;
Option(std::string name, T t, boost::optional<Menu> submenu = boost::none)
: name(name), item(t), submenu(submenu)
{ }
T choose() const;
void print(int n) const;
};
private:
template <typename U> friend struct MenuT;
friend struct visitors;
std::vector<Option> options;
//boost::optional<Menu&> parent; // TODO e.g. link_parents_visitor
enum ChoosingType { Normal, Remove };
public:
enum SpecialPosition : size_t { END_OF_MENU = size_t(-1) };
MenuT() = default;
MenuT(std::initializer_list<Option> options) : options(options) {}
void insert(const std::string &itemName, const T &t, boost::optional<Menu> submenu = boost::none, size_t pos = END_OF_MENU) {
auto it = (pos == END_OF_MENU
? options.end()
: std::next(options.begin(), std::min(pos, options.size())));
options.emplace(it, itemName, t, submenu);
}
T choose() const { return chosenItem().first; }
int print() const;
private:
std::pair<T, int> chosenItem(ChoosingType = Normal) const;
};
struct visitors {
struct simple : boost::static_visitor<>
{
void operator()(Menu& m) const { boost::apply_visitor(*this, m); }
template <typename T> void operator()(MenuT<T>& m) const {
std::cout << "-----------\n";
for (auto& o : m.options) {
std::cout << "option '" << o.name << "':\t" << o.item << "\n";
if (o.submenu)
(*this)(*o.submenu);
}
}
};
};
static const visitors::simple demo { };
int main()
{
Menu menu = MenuT<int> {
{ "one", 1 },
{ "two", 2 },
{ "three", 3 },
{ "forty-two", 42,
Menu(MenuT<std::string> {
{"Life, The Universe And Everything", "LtUae"},
{"Dent", "Arthur",
Menu(MenuT<bool> {
{"yes", true},
{"no", false},
})
},
})
},
};
std::cout << std::boolalpha;
demo(menu);
}
REMARKS
Notable omission is I don't initialize
boost::optional<Menu&> parent; // TODO e.g. link_parents_visitor
I think you still want to simplify this (I think you might not want a separation between MenuT and Option in the first place. Did you just introduce that in an attempt to nest different types of menu options?
来源:https://stackoverflow.com/questions/28011468/data-structure-with-variadic-templates