【问题标题】:Alternative to calling virtual/derived methods from constructor using callback function?替代使用回调函数从构造函数调用虚拟/派生方法?
【发布时间】:2019-01-30 21:08:53
【问题描述】:

我遇到了上述问题。我想构建一棵带有关联节点的树。由于所有树的行为相同但类型不同,因此本着继承的精神,我希望能够使用基类定义的相同树构造例程来构造具有不同类型的树。我想知道最适合我的情况的做法。

struct Tree
{
    struct Node { std::list<std::shared_ptr<Node>> children_; };

    Tree()
    {
        root_ = CreateNode();

        // carry on adding nodes to their parents...
    }

    virtual std::shared_ptr<Node> CreateNode() { return std::shared_ptr<Node>(new Node()); }

    std::shared_ptr<Node> root_;
};

struct TreeDerived : public Tree
{
    struct NodeDerived : public Tree::Node {};

    TreeDerived() : Tree() {}

    virtual std::shared_ptr<Node> CreateNode() { return std::shared_ptr<NodeDerived>(new NodeDerived()); }
};

问题是我不能调用派生函数,直到基被构造(显然),并且使用CreateNode 方法的基实现,该方法总是用基节点实现构造树。这意味着我可以在不延迟树木数量的情况下建造树木。显而易见的解决方案是将树模板化以采用不同的节点类型,使用特征强制执行节点类型?但是,这也意味着所有方法的定义都必须在标题中?这门课很丰富,所以我想尽可能避免这种情况,所以我考虑传递一个 lambda 来为我做这件事。

struct Tree
{
    struct Node { std::list<std::shared_ptr<Node>> children_; };

    Tree(std::function<std::shared_ptr<Node>()> customNodeConstructor)
    {
        root_ = customNodeConstructor();

        // carry on adding nodes to their parents... using the customNodeConstructor to create the nodes.
    }

    std::shared_ptr<Node> root_;
};

struct TreeDerived : public Tree
{
    struct NodeDerived : public Tree::Node {};

    TreeDerived(std::function<std::shared_ptr<Node>()> customNodeConstructor) : Tree(customNodeConstructor) {}
};

这允许我派生并传递与派生树相关的customNodeConstructor。由于返回的shared_ptr 对象必须从Tree::Node 派生,因此部分强制执行类型安全,尽管对派生节点类型没有强制执行。

即TreeDerived 实例化,可能应该使用TreeDerived::NodeDerived,但仅强制使用Tree::Node 或派生类型,但不一定使用TreeDerived::NodeDerived。

然后可以这样使用...

Tree tree([]() { return std::shared_ptr<Tree::Node>(); });

TreeDerived treeDerived([]() { return std::shared_ptr<TreeDerived::NodeDerived>(); });

这是一种好的做法,还是我应该在不模板化 Tree 对象的情况下做更多/其他事情?

非常感谢。

【问题讨论】:

  • 你考虑过模板吗? template&lt;class T&gt; struct Tree;?
  • @YSC 谢谢,是的,模板是解决这个问题的明显方法,但如果可以的话,我想避免这种情况,因为我想避免在标题中放入相当多的代码。
  • 您不必必须,只要您明确地使用模板将使用的所有类型专门化您的模板。见stackoverflow.com/a/495056/5470596
  • 为什么不简单地拥有不同事物的不同树?
  • @YSC 啊,好吧,不知道可以这样做。我一直认为模板的实现需要在标题中完成。不错!

标签: c++ constructor virtual c++17


【解决方案1】:

这个想法是合理的;但是,在派生类内部创建“自定义树构造函数”而不是从外部获取它会更安全。这样,您就不会得到不正确的节点类型。代码形式:

struct TreeDerived : public Tree
{
    struct NodeDerived : public Tree::Node {};

    TreeDerived() : Tree([]() { return std::make_shared<NodeDerived>(); }) {}
};

另请注意,在一般情况下,std::function 会为每个调用带来不小的运行时开销。如果你总是要传入一个无状态的 lambda,可以考虑在 Tree 构造函数中使用一个普通的函数指针:

Tree(std::shared_ptr<Node> (*customNodeConstructor)())
{
    root_ = customNodeConstructor();

    // carry on adding nodes to their parents... using the customNodeConstructor to create the nodes.
}

作为这种“传入自定义创建者”方法的替代方案,您还可以仅将 Tree 的构造函数转换为函数模板。然后它可能看起来像这样:

template <class T> struct TypeTag;

struct Tree
{
    struct Node { std::list<std::shared_ptr<Node>> children_; };

    template <class ConcreteNode>
    Tree(TypeTag<ConcreteNode>)
    {
        root_ = std::make_shared<ConcreteNode>();

        // carry on adding nodes to their parents...
    }

    std::shared_ptr<Node> root_;
};

struct TreeDerived : public Tree
{
    struct NodeDerived : public Tree::Node {};

    TreeDerived() : Tree(TypeTag<NodeDerived>{}) {}
};

然后必须在某个位置定义构造函数模板,所有从Tree 派生的类都可以看到它的定义(很可能在头文件中),但Tree 类的其余部分保持正常。

【讨论】:

  • 非常感谢,是的,这使调用者更加干净,并且可以强制执行某些类型。 :)
【解决方案2】:

我很难理解你为什么需要你做的设计(派生结构内部的结构派生自基类内部的结构看起来至少可以说是有问题的)。

话虽如此,您可以考虑 mclow 的常见问题解答中的 workarounds。

tl;dr 是您将逻辑从构造函数移动到 init() 和 init() 内部的虚函数调用“work”。

【讨论】:

  • 感谢您的回答。 “从基类内部结构派生的派生结构内部结构”纯粹是为了告知类型与其其他类型的关联,而不是污染命名空间。就设计而言,这没有什么特别的原因,只是将 Node 类型与 Tree 类型一起使用,同时允许它派生。当然这可以放在命名空间中?嵌套这些声明有问题吗?
  • 如果我正确阅读了该链接。从构造函数移动到 init 需要一个 2 阶段构造,这违反了我想强制执行的 RAII。提到的另一个选项与我使用的上述选项类似,但我没有传递一个 Helper 类,而是传递一个回调来执行“派生”逻辑。总的来说,我认为模板是这里的答案。
  • @lfgtm 阅读链接中以“第二个变体”开头的部分
猜你喜欢
  • 2019-04-19
  • 2011-09-27
  • 1970-01-01
  • 2011-03-05
  • 1970-01-01
  • 2018-07-21
  • 2012-11-06
  • 2010-10-05
相关资源
最近更新 更多