【问题标题】:How to copy a derived class with just a reference to the base class如何仅通过对基类的引用来复制派生类
【发布时间】:2020-04-25 01:47:34
【问题描述】:

我正在尝试存储对继承自外部类中 std::set 内的嵌套抽象基类的对象的引用。

让我先给你看一下代码,因为我认为这更清楚:

Class.h 接口:

#ifndef Class_H_
#define Class_H_

#include <set>
#include <memory>

class Class
{
    public:
        class AbstractBase;
    private:
        std::set< std::shared_ptr<AbstractBase>> set;
    public:
        Class();
        ~Class();

        void add(AbstractBase& object);
};

#endif

Abstract.h 接口:

#ifndef ABSTRACT_H_
#define ABSTRACT_H_

#include "Class.h"

class Class::AbstractBase
{
    friend class Class;

    public:
        virtual ~AbstractBase();
    private:
        virtual void foo(int in) = 0;
};

#endif

Derived.h 接口:

#ifndef DERIVED_H_
#define DERIVED_H_

#include "Class.h"
#include "AbstractBase.h"

class Derived : private Class::AbstractBase
{
    public:
        ~Derived() override;
    private:
        void foo(int in) override;
};

#endif

add.cc 实现:

#include "Class.h"
#include "AbstractBase.h"

void Class::add(AbstractBase& object)
{
    // create a shared pointer to object
    // and insert it in the set
    set.insert(std::make_shared<AbstractBase>(object));  // <-- this is where it fails
}

所以我会有多个不同的派生对象,它们都继承自AbstractBase,它们需要一起存储在std::set 容器中。 由于纯虚函数,编译失败。起初,我没有使用std::shared_ptr,并认为这是失败的原因,我发现this SO答案建议使用std::shared_ptr。我仍然在Class::add 中遇到编译错误,因为AbstractBase::foo 是纯的,但我认为std::shared_ptr 可以解决这个问题?

我发现了类似的问题,但没有任何东西可以帮助我解决我的具体问题。有人可以向我解释我做错了什么吗?

谢谢。

编辑:哇!感谢您提供的信息丰富的答案,我需要一些时间来彻底理解它们,看看什么最适合我。我会在完成后更新这个问题!

【问题讨论】:

  • “有人可以向我解释我做错了什么吗?”你的 add 函数似乎有错误类型的参数(逻辑上)。它应该是add( std::shared_ptr&lt;AbstractBase&gt; obj )
  • 基本上问题是您的add() 方法应该获得传递给它的对象的所有权,尽管在技术上可以通过引用来做到这一点,但这不是一个正确的解决方案。您需要通过传递智能指针来转移所有权。
  • 谢谢,我尝试了您建议的不同变体,但这也没有编译。你确定你的建议是对的吗?是的,我还在学习和阅读很多东西:)
  • 有哪些变化?将参数类型更改为std::shared_ptr 并直接插入该指针而不使用std::make_shared 语句(最好使用std::move
  • @Slava 我会考虑你的建议! @aschepler 我不想创建一个新对象。目的是当某个事件发生时(所以我将容器更改为std::map&lt;int, std::set&lt;shared_ptr&gt;&gt;)我遍历映射中的对象并调用映射到事件的容器中对象的所有foo() 函数。 @Max Langhof 由于上述原因,我需要该集合,两次在容器中放置一个对象没有意义,这就是我选择 std::set 的原因。

标签: c++ inheritance


【解决方案1】:

您的函数试图做的是复制一个对象,为该副本分配一个共享实例,并存储指向该副本的指针。

由于您的意图是在集合中“存储引用”,因此您可能打算将对象存储在其他地方并且实际上并不想要副本。如果那些引用的对象是共享的,那么正确的解决方案是传递共享指针:

void Class::add(std::shared_ptr<AbstractBase> ptr)
{
    set.insert(std::move(ptr));
}

如果引用的对象不是共享的,那么您不能使用共享指针来引用它们。您可以改用非拥有指针:

std::set<AbstractBase*> set;

void Class::add(AbstractBase* ptr);

但是,在使用非拥有方法时要非常小心,至少只要它们被集合引用,就可以让被引用的对象保持活动状态。这是不平凡的。引用可以用作add 的参数,但我建议不要这样做,因为对于调用者来说,函数将指向参数的指针存储的时间可能比函数执行的时间长。


如果你确实想复制,那么你可以使用一个返回共享指针的虚函数。示例:

class Class::AbstractBase
{
    public:
        virtual std::shared_ptr<AbstractBase> copy() = 0;
// ...

class Derived : private Class::AbstractBase
{
    public:
        std::shared_ptr<AbstractBase> copy() override {
            auto ptr = std::make_shared<Derived>(*this);
            return {ptr, static_cast<Class::AbstractBase*>(ptr.get())};
        }
// ...

void Class::add(AbstractBase& object)
{
    set.insert(object.copy());

为避免在多个派生类型中重复相同的copy,您可以使用 CRTP。

【讨论】:

  • 谢谢!做了一个快速测试,它奏效了。但是,我想保持签名原样,这是一个具有预定义签名的大型项目的一部分。 add() 函数接收对派生对象的引用,然后需要将其存储在外部类的容器中。
  • @pjterlustig 在这种情况下考虑使用std::ref 并存储结果std::reference_wrappers。
  • @pjterlustig 如果您愿意,您可以将引用参数用于非拥有方法,但我建议您不要存储引用/指向通过引用传递的参数的指针。只有在引用的存储时间明显长于函数持续时间的情况下才应该这样做。错误的假设将导致危险的指针/引用。
【解决方案2】:

您需要明确存储在集合中的对象的所有权。如果您使用std::shared_ptr,则基本上将每个Class 内的set 编码为拥有 包含的实例。这与add(AbstractBase&amp;) 方法不兼容——您不能真正通过引用获得任意对象的所有权。如果这个对象已经被另一个 shared_ptr 管理了怎么办?

也许您实际上只想在集合中存储副本。在这种情况下,请参阅其他答案以了解多态复制(“克隆”)对象的方法。

你为什么要使用std::set也是公开的。 std::set 使用 &lt; 运算符(或用户提供的具有等效语义的比较函子)建立所包含对象的唯一性。你甚至想要独特性吗?如果是这样,基于什么标准?目前,没有办法比较存储的类对象。 std::shared_ptr 通过比较内部指针值“解决”了这个问题,但我怀疑这就是你需要的。

如果您真的想仅根据对象的内存位置来存储和比较对象,并且假定存储对象的所有权,则可以只使用原始指针。如果您只想存储一大堆对象而不关心唯一性(因为您当前尝试创建副本,每个存储的元素都有一个唯一的地址,因此您目前永远不会使用std::set 的那个方面),也许@987654332 @ 是更好的解决方案。 std::set 还可以帮助有效地确定对象是否存在于集合中,但std::vector 可以做到这一点(如果您排序和二进制搜索,可能更快)。考虑http://lafstern.org/matt/col1.pdf 中的建议。

【讨论】:

  • 与指针相比,存储std::reference_wrapper 对象可能更方便——当然,这仍然取决于实际所有者的生命周期管理。
  • @Max Langhof 感谢您的解释!我不想比较,所以我会考虑使用std::vector。我选择了std::set,因为我想遍历集合并在某个事件上调用每个派生的objs foo() 函数。这样做两次没有意义,因此我选择了这组。
【解决方案3】:

如果要复制一个未知动态类型的类,有三种众所周知的方式to get around having insufficient information

  1. 有一种方法可以将对象映射到期望特定最派生类型的处理程序。 typeid、成员函数或公共基类中的数据成员,这是其静态类型最常用的。这很费时间,而且设置起来很麻烦,但至少您不必经常修改类或使用胖指针。

  2. 有一个函数来.clone() 静态已知基中的对象。这被称为虚拟构造函数,通常是最有效和最方便的设置。

  3. 围绕一个额外的指针进行克隆。这对类型或附加设置的侵入性最小,但会改变界面。

哪个最合适由您决定。
也就是说,如果您确实想要复制对象,并且不应该将 shared_ptr 传递给 .add()

【讨论】:

  • 感谢您提供的信息,这不正是我要找的东西,但因为我是 C++ 新手,所以肯定很有趣!正如@Slava 所写,我不想复制,我正在寻找一种方法来将派生对象的不同引用/指针/智能指针存储在一个容器中,以便我可以从Class 调用它们的虚函数。 add() 函数应该会收到一个引用。
  • @pjterlustig 如果您不想复制,它不应该收到参考。
猜你喜欢
  • 2019-04-05
  • 1970-01-01
  • 2021-12-05
  • 2018-06-28
  • 2011-03-23
  • 2019-08-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多