【问题标题】:Data structure with variadic templates具有可变参数模板的数据结构
【发布时间】:2015-01-18 15:36:06
【问题描述】:

我有一个Menu<T> 类,它的选项是T 类型的项目,它可能有Menu<T> 类型的子菜单(嵌套子菜单的深度没有限制)。

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

我已经测试过它可以正常工作,但是我上面的 Menu&lt;T&gt; 类不支持其项目类型不同于 T 的子菜单。这个额外的功能会非常方便,如果说主菜单有 @ 987654325@ 作为其选项的类型,然后其中一个选项是“取出武器”,其子菜单理想情况下希望将 Weapon 作为其选项,但就像现在一样,子菜单必须再次具有Action 作为它的选项。

我试图概括

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.
}

不正确,因为Menu&lt;int, std::string&gt; menu; 我试图用字符串选项的子菜单创建一个简单的 int 选项菜单,甚至无法编译,因为子菜单的类型为 Menu&lt;std::string&gt;,与类不匹配模板。这也没有意义,因为Menu&lt;int, std::string&gt; 是从其choose() 函数返回int,但进入其子菜单将返回一个字符串。这里需要 boost::variant 吗?

我只需要有人指出如何开始。我知道这似乎属于 CodeReview,但他们只想检查我的代码是否已经在工作,但在这里我的泛化尝试还远未奏效(甚至还没有开始),所以我需要呼吁这里的专家就如何开始。

更新: 根据 gmbeard 的建议,我使用以下简化代码(真正的 Menu 类将具有链接的选项列表,用户将通过输入从中选择)。但也有缺点。

#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.
}

输出:

You chose t.

最大的问题是ChooseVisitor 需要针对所有可能的菜单选项类型不断更新(它最终可能会产生数百个数据成员和重载),更不用说可怕的 if 检查以获得所需的返回物品。但是选择的项目需要存储,而不仅仅是短期使用。我欢迎提出改进意见。

【问题讨论】:

  • 您应该考虑使用继承并拥有Menu&lt;T&gt; 的基类,例如BaseMenu,然后子菜单可以是指向此基类的指针...
  • 你推荐使用 boost::any 作为 choose() 函数的返回类型吗?
  • 为什么你希望它可以有不同类型的子菜单,而主菜单项必须都是相同的类型?这似乎是一组奇怪的限制,真的。
  • 子菜单中的选项也都是相同的类型,但该类型可能与主菜单的选项类型不同。第一个动机来自我已经为Menu&lt;T&gt; 提供的“返回上一个菜单”选项。就像现在一样,每当我想通过更改 T 类型(当前必须是新菜单而不是子菜单)前进到不同的菜单时,我无法“返回”到以前的菜单不同种类。将所有菜单作为子菜单放在根菜单中可以解决这个问题。

标签: c++ templates c++11 variadic


【解决方案1】:

一种解决方案是创建一些Menu 的部分特化来解开可变参数包。

首先,创建模板类...

// Can be incomplete; only specialized versions will be instantiated...
template<typename... Args> class Menu;

现在,为菜单链的末端创建一个特化(无子菜单)...

template<typename T>
class Menu<T>
{
public:
  // No submenu in this specialization
  using item_type = T;
  std::vector<item_type> items;
  ...
};

最后,为具有子菜单的Menu 实例创建一个特化...

template<typename Head, typename... Tail>
class Menu<Head, Tail...>
{
public:
  Menu<Tail...> submenu;
  using item_type = Head;
  std::vector<item_type> items;

  ...
};

为简洁起见,这是您的类的简化版本,但如果您添加嵌套的 Option 类,同样的原则仍然适用。

您可以使用类似的技术通过重载非成员函数来递归子菜单...

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

更新:choose() 功能的可能实现可以使用访问者模式。您需要为菜单中包含的每种类型提供重载 operator() 的类型(在本例中为 intstd::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 */ }
};

类似于print_menu 函数,您可以定义几个choose_menu 函数重载...

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

这会像这样使用......

Menu<int, std::string> menu{};
...
choose_menu(menu, ChooseVisitor{});

要得出您对 choose() 函数的想法有点困难,但您应该能够调整上述内容以适应大多数情况。

【讨论】:

  • 好建议!我按照你的想法,一切似乎都很好,包括 print(),除了最重要的菜单函数 T choose() const,它返回从菜单中选择的项目(可能来自其子菜单)?返回类型可能不是 T,因为这完全取决于输入的子菜单。我们是否需要使用 boost:any,如果需要,究竟如何?我原始问题中的更新显示了新代码来说明我所指的问题。
  • 考虑到您知道您的菜单/子菜单将包含哪些类型,您可以使用变体类型。另一种选择是使用访客。我无法真正弄清楚您的 choose() 函数是如何工作的。它看起来是确定性的,因为它不需要参数。
  • 我已更新答案以澄清我的访客建议
  • 好的,所以我决定choose() 函数所有Menu&lt;...&gt; 实例都将返回ChooseVisitor,其存储的项目是所需的选定项目。我这样做是因为提取的项目需要长期存储,而不仅仅是像您上面建议的那样用于短期。我已将我的最新代码放在我的开场问题中,您可能希望查看该代码以了解 choose() 函数背后的意图。但我也提到了该解决方案的缺点。如果您仍然感兴趣,我欢迎进一步改进。非常感谢到目前为止所做的一切。
【解决方案2】:

我有一种轻微的感觉,这里的东西被过度设计了。

如您所料,这是一个使用 Boost Variant 的版本。

我简化了一些事情。我特别喜欢初始化列表构造函数,因此您可以简单地递归地构造菜单树,如下所示:

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

如您所见,它在不同级别混合了MenuT&lt;int&gt;MenuT&lt;std::string&gt;MenuT&lt;bool&gt;。您可以毫不费力地访问它:

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

打印出来的

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

备注

  • 值得注意的遗漏是我没有初始化

    boost::optional<Menu&> parent; // TODO e.g. link_parents_visitor
    
  • 我认为您仍然想要简化这一点(我认为您可能不希望首先将 MenuT 和 Option 分开。您是否只是为了尝试嵌套不同的菜单选项的类型?

【讨论】:

    猜你喜欢
    • 2021-07-01
    • 2019-03-23
    • 2019-08-04
    • 2014-11-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多