【问题标题】:std::unique_ptr, Default Copy Constructor, and Abstract Classstd::unique_ptr、默认复制构造函数和抽象类
【发布时间】:2014-08-04 23:23:24
【问题描述】:

我有一个类表示一个使用唯一指针的树对象,构成树的一些节点,以及一个基于一些参数构造指向抽象节点类的指针的函数(它使指针指向子类,因为抽象节点是抽象的)

class AbstractNode
{
    vector<unique_ptr<AbstractNode>> children;
public:
    AbstractNode(arguments...);
    // other stuff...
};
class Tree
{
    unique_ptr<AbstractNode> baseNode;
    // other stuff...
}
unique_ptr<AbstractNode> constructNode(AbstractNodeTypes type);

abstractNode 的各种子类将包含在树中。子类为该类中的某些虚函数提供了不同的实现。

我希望能够通过创建一组具有相同类类型的新节点来复制我的树,这些节点是原始树中节点的不同副本。


这是问题:

如果我为 AbstractNode 类编写自己的复制构造函数来深度复制子类,我将不得不为 AbstractNode 的所有子类编写复制构造函数,这似乎很烦人,因为唯一不会正确复制的是子指针。在这里使用复制构造函数也会很烦人,因为我认为我需要在调用子项之前将它们转换为正确的类型。

有什么方法可以让编译器让我使用默认的复制构造函数来设置除子对象之外的所有内容。它可以将它们保留为空指针或其他东西吗?然后我可以编写一个更简单的函数,它只是递归地添加子节点来复制一棵树。

如果这是不可能的,有没有任何人知道的解决这个问题的非丑陋解决方案?

【问题讨论】:

  • 所以你想要来自默认复制构造函数的非默认行为?为什么还要复制一个容器?
  • @SchizoidSpag 树开始时有 1 个节点,然后我通过将符合特定条件的所有可能节点添加到已经存在的节点来构建树列表。如果不复制树并添加新的孩子,我不知道该怎么做。
  • 容器真的这么大这么复杂,有很多数据应该默认复制吗?
  • 直接添加到树,通过引用而不是通过值复制,或使用移动语义添加其子级是否有问题?如果不需要维护树的多个副本,则不需要复制构造函数,而是需要更好的设计。
  • @Deduplicator 节点复杂,每个子类都不一样。容器并不是很复杂,尽管它记录了很多东西。我只是在这里发布了简单的框架,以便该问题易于阅读,并且对阅读它的人尽可能广泛有用。

标签: c++ c++11


【解决方案1】:

解决此问题的典型方法是使用类似于 Kerrek SB 在其回答中描述的虚拟 clone 函数。但是,我不会费心编写您自己的 value_ptr 课程。当您的问题出现时,仅重用 std::unique_ptr 会更简单。它将需要AbstractNode 中的非默认复制构造函数,但不需要显式或不安全的强制转换:

class AbstractNode
{
    std::vector<std::unique_ptr<AbstractNode>> children;
public:
    AbstractNode() = default;
    virtual ~AbstractNode() = default;
    AbstractNode(AbstractNode const& an)
    {
        children.reserve(an.children.size());
        for (auto const& child : an.children)
            children.push_back(child->clone());
    }

    AbstractNode& operator=(AbstractNode const& an)
    {
        if (this != &an)
        {
            children.clear();
            children.reserve(an.children.size());
            for (auto const& child : an.children)
                children.push_back(child->clone());
        }
        return *this;
    }

    AbstractNode(AbstractNode&&) = default;
    AbstractNode& operator=(AbstractNode&&) = default;
    // other stuff...

    virtual
        std::unique_ptr<AbstractNode>
        clone() const = 0;
};

现在可以实现ConcreteNode。它必须有一个有效的复制构造函数,这取决于 ConcreteNode 添加到组合中的数据成员。而且它必须实现clone(),但实现起来很简单:

class ConcreteNode
    : public AbstractNode
{
public:
    ConcreteNode() = default;
    virtual ~ConcreteNode() = default;
    ConcreteNode(ConcreteNode const&) = default;
    ConcreteNode& operator=(ConcreteNode const&) = default;
    ConcreteNode(ConcreteNode&&) = default;
    ConcreteNode& operator=(ConcreteNode&&) = default;
    // other stuff...

    virtual
        std::unique_ptr<AbstractNode>
        clone() const override
        {
            return std::unique_ptr<AbstractNode>(new ConcreteNode(*this));
        }
};

我建议让 clone 返回 unique_ptr 而不是原始指针,以确保不会在没有所有者的情况下暴露新的指针。

为了完整起见,我还展示了其他特殊成员的外观。

起初我认为 C++14 的 make_unique 在这里使用会很好。它可以在这里使用。但就我个人而言,我认为在这个特定的例子中,它真的没有承载它的重量。 Fwiw,这就是它的样子:

    virtual
        std::unique_ptr<AbstractNode>
        clone() const override
        {
            return std::make_unique<ConcreteNode>(*this);
        }

使用make_unique,您必须首先构造一个unique_ptr&lt;ConcreteNode&gt;,然后依赖从它到unique_ptr&lt;AbstractNode&gt; 的隐式转换。这是正确的,一旦完全启用内联,额外的舞蹈可能就会被优化掉。但是,make_unique 的使用在这里似乎是一种不必要的混淆,因为您真正需要的是用新的ConcreteNode* 构造的unique_ptr&lt;AbstractNode&gt;。

【讨论】:

  • 我还在读这个,但从我目前所读的内容来看……首先,新的 ConcreteNode(*this) 调用最终会调用 AbstractNode 复制构造函数吗?如果发生这种情况,我想我明白你在说什么。其次,我是否正确,我仍然需要在复制构造函数中手动复制 AbstractNode 类中的所有成员?或者你是否建议如果我将抽象节点中的所有成员都放入我希望默认复制到具体节点中,然后使用具体节点的进一步子类和它们自己的克隆函数,我可以避免这种情况?
  • 另外,我一直在使用 make_unique。我认为它是 C++ 11 的一部分。我的 Visual Studio Express 2013 编译器似乎允许我使用它,所以如果你想在你的答案中使用它,我认为它适用于我。
  • 我不确定重新发明所有这些轮子是否是个好建议。用户应该很少编写析构函数,并且只有在绝对需要时才这样做。除了重新发明向量的复制构造函数之外,您无缘无故地引入原始循环。我不认为这是一种可扩展的心态。
  • @user3281410:是的,ConcreteNode 复制构造函数将隐式调用AbstractNode 复制构造函数。是的,AbstractNode 复制构造函数需要用户定义,如上所示除非您想编写 value_ptr,如 Kerrek SB 的回答中所述。我认为只编写复制构造函数更容易。但是最终我们可能会得到一些标准提供的value_ptr,然后使用起来会更容易。
  • 我赞成 Kerrek SB 的回答,因为这也是一种有效的方法。作为unique_ptr 的发明者,我可以证明,编写自己的智能指针,即使像unique_ptr 和value_ptr 这样简单的东西也很难做到正确。
【解决方案2】:

您可能希望推出自己的value_ptr 实现,而不是使用unique_ptr。这些设计在过去经常被提出,但在我们有一个标准化版本之前,要么自己动手,要么找到一个现有的实现。

看起来有点像这样:

template <typename T> struct value_ptr
{
    T * ptr;
    // provide access interface...

    explicit value_ptr(T * p) noexcept : ptr(p) {}

    ~value_ptr() { delete ptr; }

    value_ptr(value_ptr && rhs) noexcept : ptr(rhs.ptr)
    { rhs.ptr = nullptr; }

    value_ptr(value_ptr const & rhs) : ptr(rhs.clone()) {}

    value_ptr & operator=(value_ptr && rhs) noexcept
    {
        if (&rhs != this) { delete ptr; ptr = rhs.ptr; rhs.ptr = nullptr; }
        return *this;
    }

    value_ptr & operator=(value_ptr const & rhs)
    {
        if (&rhs != this) { T * p = rhs.clone(); delete ptr; ptr = p; }
        return *this;
    }

};

您可以从一组可克隆节点构建您的树。

struct AbstractNode
{
    virtual ~AbstractNode() {}
    virtual AbstractNode * clone() const = 0;

    std::vector<value_ptr<AbstractNode>> children;
};

struct FooNode : AbstractNode
{
    virtual FooNode * clone() const override { return new FooNode(this); }
    // ...
};

现在您的节点可以自动复制,而无需您编写任何显式的复制构造函数。您需要做的就是通过在每个派生类中重写 clone 来保持纪律。

【讨论】:

  • 我明白这如何立即解决我的问题。我对开设自己的课程持谨慎态度,因为我仍在学习 C++ 的细节,而您在其中使用的一些语言元素超出了我的范围。有没有这样的图书馆?此外,在我的特殊情况下,我有一个大型函数可以自动构造正确类型的节点。它需要一个 NodeType 枚举,并有一个大的 switch 语句,其中包含“case ... : return make_unique(...)”之类的行。我可以使用它来将克隆函数保留在抽象类中,而不是将其添加到每个子类节点中吗?
【解决方案3】:

通常的模式是通过你的层次结构的虚拟克隆方法。

如果这是不可能的,是否有任何人都知道的解决这个问题的非丑陋解决方案?

您还可以使用基于复制构造函数的克隆函数的模板实例化。这是我在正在编写的 Web 服务器中使用的解决方案(宠物项目):

#pragma once

#include <memory>
#include <cassert>
#include <functional>
#include <stdexcept>
#include <vector>

namespace stdex {
    inline namespace details {

        /// @brief Deep copy construct from (Specialized&)*src
        ///
        /// @retval nullptr if src is nullptr
        /// @retval Specialized clone of *src
        ///
        /// @note Undefined behavior src does not point to a Specialized*
        template<typename Base, typename Specialized>
        Base* polymorphic_clone (const Base* src) {
            static_assert(std::is_base_of<Base, Specialized>::value,
                "Specialized is not a specialization of Base");

            if (src == nullptr)
                return nullptr;
            return new Specialized{ static_cast<const Specialized&>(*src) };
        }
    }

    /// @brief polymorphic reference interface over a base class
    ///
    /// Respects polymorphic behavior of class ref.
    /// Instances have deep copy semantics (clone) and
    /// "[const] Base&" interface
    ///
    /// @note Not regular: no trivial way to implement non-intrusive equality
    ///
    /// @note safe to use with standard containers
    template<typename Base>
    class polymorphic final
    {
    public:

        /// Functor capable to convert a Base* to it's specialized type
        /// and clone it (intrusive implementation can be used)
        typedef std::function<Base* (const Base*)>  clone_functor;

        /// @brief construct (takes ownership of ptr)
        template<typename Specialized, typename CloneSpecialized>
        polymorphic(Specialized* ptr, CloneSpecialized functor) noexcept
        : instance_{ptr}, clone_{std::move(functor)}
        {
            static_assert(std::is_base_of<Base, Specialized>::value,
            "Specialized is not a specialization of Base");
            static_assert(
            std::is_constructible<clone_functor, CloneSpecialized>::value,
            "CloneSpecialized is not valid for a clone functor");
        }

        // not implemented: UB cloning in case client provides specialized ptr
        // polymorphic(Base* ptr);

        polymorphic()
        : polymorphic{ nullptr, clone_functor{} }
        {
        }

        polymorphic(polymorphic&&) = default;

        polymorphic(const polymorphic& other)
        : polymorphic{std::move(other.clone())}
        {
        }
        polymorphic& operator=(polymorphic other)
        {
            std::swap(instance_, other.instance_);
            std::swap(clone_, other.clone_);
            return *this;
        }

        ~polymorphic() = default;

        /// @brief Cast to contained type
        /// @pre instance not moved
        /// @pre *this initialized with valid instance
        operator Base&() const
        {
            assert(instance_.get());
            return *instance_.get();
        }

        /// @brief Cast to contained type
        /// @pre instance not moved
        /// @pre *this initialized with valid instance
        operator const Base&() const
        {
            assert(instance_.get());
            return *instance_.get();
        }

    private:
        polymorphic clone() const
        {
            return polymorphic{
                clone_(instance_.get()), clone_functor{clone_}
            };
        }

        std::unique_ptr<Base>   instance_;
        clone_functor           clone_;
    };

    template<typename Base, typename Specialized, typename CF>
    polymorphic<Base> to_polymorphic(Specialized&& temp, CF functor)
    {
        static_assert(std::is_base_of<Base, Specialized>::value,
        "Specialized is not a specialization of Base");

        typedef typename polymorphic<Base>::clone_functor clone_functor;

        auto    ptr_instance = std::unique_ptr<Base>{
            new Specialized{std::move(temp)}
        };
        auto    clone_instance = clone_functor{std::move(functor)};

        return polymorphic<Base>{ptr_instance.release(), clone_instance};
    }

    template<typename Base, typename Specialized>
    polymorphic<Base> to_polymorphic(Specialized&& temp)
    {
        static_assert(std::is_base_of<Base, Specialized>::value,
        "Specialized is not a specialization of Base");

        return to_polymorphic<Base,Specialized>(
            std::move(temp), details::polymorphic_clone<Base,Specialized>
        );
    }

    template<typename Base, typename Specialized, typename ...Args>
    polymorphic<Base> to_polymorphic(Args ...args)
    {
        static_assert(std::is_constructible<Specialized, Args...>::value,
        "Cannot instantiate Specialized from arguments");

        return to_polymorphic<Base,Specialized>(
            std::move(Specialized{std::forward<Args...>(args...)}));
    }

    template<typename Base> using polymorphic_vector =
    std::vector<polymorphic<Base>>;

    template<typename Base, typename ...Args>
    polymorphic_vector<Base> to_polymorphic_vector(Args&& ...args)
    {
        return polymorphic_vector<Base>{to_polymorphic<Base>(args)...};
    }

} // stdex

使用示例:

stdex::polymorphic_vector<view> views = // explicit type for clarity
    stdex::to_polymorphic_vector(
        echo_view{"/echo"}, // class echo_view : public view
        directory_view{"/static_files"} // class directory_view : public view
    );

for(auto& v: views)
    if(v.matches(reuqest.url())) // bool view::matches(...);
        auto response = view.handle(request); // virtual view::handle(...) = 0;

这个限制:

如果您使用多重继承,请勿使用 stdex::details::polymorphic_clone。改为基于 dynamic_cast 编写实现,并使用to_polymorphic(Specialized&amp;&amp; temp, CF functor)。

【讨论】:

    【解决方案4】:

    如果您想对类的一部分使用默认行为,而只对其余部分使用非标准行为来增强它,请考虑在功能上和组织上拆分您的类:

    将所有需要默认行为的元素放入它们自己的子对象(继承或复合)中,这样您就可以轻松地为它们使用默认特殊功能,并将其余部分添加到该子对象之外。

    实现留给感兴趣的读者作为练习。

    【讨论】:

    • 我试图了解我将如何做到这一点;看起来很简单。但是节点需要一直访问他们的孩子,所以具有默认构造函数的子对象需要以某种方式访问​​孩子,所以它需要某种指针,我看不到我怎样才能通过这种方式避免这个问题?
    • 如果你使用继承,你总是可以使用static_cast 来获取父级。您可能会考虑为此使用CRTP,尽管这可能过于矫枉过正。
    猜你喜欢
    • 1970-01-01
    • 2022-12-04
    • 2018-09-16
    • 1970-01-01
    • 2020-05-14
    • 2016-05-07
    • 1970-01-01
    • 2010-12-02
    • 2011-07-21
    相关资源
    最近更新 更多