将std :: vector存储在主机父班中的最佳方法

发布于 2025-01-29 12:04:11 字数 1299 浏览 3 评论 0原文

我想在主机类中存储一个std :: vector<>包含具有共同基类的对象。主机类应保持共配,因为它存储在其所有者类的std :: vector<>中。

C ++提供了多种方法,但我想知道最佳实践。

这是一个示例,使用std :: shared_ptr>>

class Base{};
class Derivative1: public Base{};
class Derivative2: public Base{};

class Host{
public: std::vector<std::shared_ptr<Base>> _derivativeList_{};
};

class Owner{
public: std::vector<Host> _hostList_;
};

int main(int argc, char** argv){
  Owner o;
  o._hostList_.resize(10);
  
  Host& h = o._hostList_[0];
  h._derivativeList_.emplace_back(std::make_shared<Derivative1>());
  // h._derivativeList_.resize(10, std::make_shared<Derivative1>()); // all elements share the same pointer, but I don't want that. 
}

这里的主要缺点是为了在_derivativelist _ 我需要为每个元素执行emplace_back()。这比简单的ressize(n)要花费更多的时间,该我无法与std :: shared_ptr&gt;&gt;一起使用,因为它将为其创建相同的指针实例每个插槽。

我考虑过使用std :: simolor_ptr&lt;&gt;,但这是不可行的,因为它使host class class class nonable(> std请求的功能: :vector)。

否则,我可以使用std :: variant&lt; dedield1,derived2&gt;可以做我想要的。但是,我需要声明派生类的每一个可能的实例...

对此有任何想法/建议吗?

I want to store a std::vector<> containing objects which have a common base class, within a host class. The host class should remain copiable since it is stored inside a std::vector<> of it's owner class.

C++ offers multiple ways of doing that, but I want to know the best practice.

Here is an example using std::shared_ptr<>:

class Base{};
class Derivative1: public Base{};
class Derivative2: public Base{};

class Host{
public: std::vector<std::shared_ptr<Base>> _derivativeList_{};
};

class Owner{
public: std::vector<Host> _hostList_;
};

int main(int argc, char** argv){
  Owner o;
  o._hostList_.resize(10);
  
  Host& h = o._hostList_[0];
  h._derivativeList_.emplace_back(std::make_shared<Derivative1>());
  // h._derivativeList_.resize(10, std::make_shared<Derivative1>()); // all elements share the same pointer, but I don't want that. 
}

Here the main drawback for me is that in order to claim a lot of elements in _derivativeList_ I need to perform emplace_back() for every single element. This takes a lot more time than a simple resize(N) which I can't use with std::shared_ptr<> since it will create the same pointer instance for every slot.

I thought about using std::unique_ptr<> instead, but this is not viable since it makes the Host class non copiable (a feature requested by std::vector).

Otherwise, I could use std::variant<Derived1, Derived2> which can do what I want. However I would need to declare every possible instance of the derived class...

Any thought/advice about this?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

毅然前行 2025-02-05 12:04:11

TLDR:根据上下文使用变体或类型擦除。

您在C ++中要求的内容将被描述为 是一个值类型或具有价值语义的类型。您需要一种可复制的类型,并仅复制“做正确的事”(副本不共享所有权)。但是与此同时,您想要多态性。您想拥有各种满足相同界面的类型。因此...多态值类型。

价值类型更易于使用,因此它们将使界面更加愉快。但是,它们实际上可能会更糟,并且实施更为复杂。因此,与一切一样,酌处权和判断力发挥作用。但是我们仍然可以谈论实施它​​们的“最佳实践”。

让我们添加一个接口方法,以便我们可以说明以下一些相对优点:

struct Base {
  virtual ~Base() = default;
  virtual auto name() const -> std::string = 0;
};

struct Derivative1: Base {
  auto name() const -> std::string override {
    return "Derivative1";
  }
};

struct Derivative2: Base {
  auto name() const -> std::string override {
    return "Derivative2";
  }
};

有两种常见的方法:变体和类型擦除。这些是我们在C ++中的最佳选择。

正如您所说,变体

是当类型集有限且关闭时的最佳选择。预计其他开发人员不会以自己的类型添加到集合中。

using BaseLike = std::variant<Derivative1, Derivative2>;

struct Host {
  std::vector<BaseLike> derivativeList;
};

直接使用该变体的缺点是:BASELIKE的作用不像base。您可以复制它,但不能实现接口。任何使用它都需要访问。

因此,您可以用一个小包装器将其包装:

class BaseLike: public Base {
public:
  BaseLike(Derivative1&& d1) : data(std::move(d1)) {}
  BaseLike(Derivative2&& d2) : data(std::move(d2)) {}

  auto name() const -> std::string override {
    return std::visit([](auto&& d) { return d.name(); }, data);
  }

private:
  std::variant<Derivative1, Derivative2> data;
};

struct Host {
  std::vector<BaseLike> derivativeList;
};

现在您有了一个列表,您可以同时将derivativation1derivative2 和对待元素的引用,就像任何<代码>基础&amp; 。

有趣的是,base没有提供太多价值。借助抽象方法,您知道所有派生的类都正确实现了它。但是,在这种情况下,我们知道所有派生的类,如果它们无法实现该方法,则访问将无法编译。因此,基本实际上没有提供任何值。

struct Derivative1 {
  auto name() const -> std::string {
    return "Derivative1";
  }
};

struct Derivative2 {
  auto name() const -> std::string {
    return "Derivative2";
  }
};

如果我们需要谈论界面,我们可以通过定义一个概念来做到这一点:

template <typename T>
concept base_like = std::copyable<T> && requires(const T& t) {
  { t.name() } -> std::same_as<std::string>;
};

static_assert(base_like<Derivative1>);
static_assert(base_like<Derivative2>);
static_assert(base_like<BaseLike>);

最后,此选项看起来像: https://godbolt.org/z/7yw9fpv6y

类型擦除

假设我们有一组开放的类型。

经典和最简单的方法是在指针中流量或引用通用基类。如果您还想要所有权,请将其放入unique_ptr中。 (shared_ptr不太合适。)然后,您必须实现复制操作,因此将unique_ptr放入包装器类型中并定义复制操作。经典方法是将方法定义为基类接口的一部分clone(),每个派生的类都覆盖以复制自身。 unique_ptr包装器可以在需要复制时调用该方法。

这是一种有效的方法,尽管它具有一些权衡。需要基础类是侵入性的,如果您同时想满足多个接口,可能会很痛苦。 std :: vector&lt; t&gt; and std :: set&lt; t&gt;不共享普通基类,但两者都可以。另外,clone()方法是纯样板。

类型擦除将这一步骤更进一步,并消除了对普通基类的需求。

在这种方法中,您仍然定义一个基类,但是对于您而不是用户:

struct Base {
  virtual ~Base() = default;
  virtual auto clone() const -> std::unique_ptr<Base> = 0;
  virtual auto name() const -> std::string = 0;
};

并且定义了作为特定类型特定委托的实现。同样,这是给您的,而不是您的用户:

template <typename T>
struct Impl: Base {
  T t;
  Impl(T &&t) : t(std::move(t)) {}
  auto clone() const -> std::unique_ptr<Base> override {
    return std::make_unique<Impl>(*this);
  }
  auto name() const -> std::string override {
    return t.name();
  }
};

然后您可以定义用户与用户交互的类型类型:

class BaseLike
{
public:
  template <typename B>
  BaseLike(B &&b)
    requires((!std::is_same_v<std::decay_t<B>, BaseLike>) &&
             base_like<std::decay_t<B>>)
  : base(std::make_unique<detail::Impl<std::decay_t<B>>>(std::move(b))) {}

  BaseLike(const BaseLike& other) : base(other.base->clone()) {}

  BaseLike& operator=(const BaseLike& other) {
    if (this != &other) {
      base = other.base->clone();
    }
    return *this;
  }

  BaseLike(BaseLike&&) = default;

  BaseLike& operator=(BaseLike&&) = default;

  auto name() const -> std::string {
    return base->name();
  }

private:
  std::unique_ptr<Base> base;
};

最后,此选项看起来像: https://godbolt.org/z/p3zt9nb5o

tldr: Use a variant or type erasure, depending on context.

What you are asking for in C++ would be described roughly as a value type or a type with value semantics. You want a type that is copyable, and copying just "does the right thing" (copies do not share ownership). But at the same time you want polymorphism. You want to hold a variety of types that satisfy the same interface. So... a polymorphic value type.

Value types are easier to work with, so they will make a more pleasant interface. But, they may actually perform worse, and they are more complex to implement. Therefore, as with everything, discretion and judgment come into play. But we can still talk about the "best practice" for implementing them.

Let's add an interface method so we can illustrate some of the relative merits below:

struct Base {
  virtual ~Base() = default;
  virtual auto name() const -> std::string = 0;
};

struct Derivative1: Base {
  auto name() const -> std::string override {
    return "Derivative1";
  }
};

struct Derivative2: Base {
  auto name() const -> std::string override {
    return "Derivative2";
  }
};

There are two common approaches: variants and type erasure. These are the best options we have in C++.

Variants

As you imply, variants are the best option when the set of types is finite and closed. Other developers are not expected to add to the set with their own types.

using BaseLike = std::variant<Derivative1, Derivative2>;

struct Host {
  std::vector<BaseLike> derivativeList;
};

There's a downside to using the variant directly: BaseLike doesn't act like a Base. You can copy it, but it doesn't implement the interface. Any use of it requires visitation.

So you would wrap it with a small wrapper:

class BaseLike: public Base {
public:
  BaseLike(Derivative1&& d1) : data(std::move(d1)) {}
  BaseLike(Derivative2&& d2) : data(std::move(d2)) {}

  auto name() const -> std::string override {
    return std::visit([](auto&& d) { return d.name(); }, data);
  }

private:
  std::variant<Derivative1, Derivative2> data;
};

struct Host {
  std::vector<BaseLike> derivativeList;
};

Now you have a list in which you can put both Derivative1 and Derivative2 and treat a reference to an element as you would any Base&.

What's interesting now is that Base is not providing much value. By virtue of the abstract method, you know that all derived classes correctly implement it. However, in this scenario, we know all the derived classes, and if they fail to implement the method, the visitation will fail to compile. So, Base is actually not providing any value.

struct Derivative1 {
  auto name() const -> std::string {
    return "Derivative1";
  }
};

struct Derivative2 {
  auto name() const -> std::string {
    return "Derivative2";
  }
};

If we need to talk about the interface we can do so by defining a concept:

template <typename T>
concept base_like = std::copyable<T> && requires(const T& t) {
  { t.name() } -> std::same_as<std::string>;
};

static_assert(base_like<Derivative1>);
static_assert(base_like<Derivative2>);
static_assert(base_like<BaseLike>);

In the end, this option looks like: https://godbolt.org/z/7YW9fPv6Y

Type Erasure

Suppose instead we have an open set of types.

The classical and simplest approach is to traffic in pointers or references to a common base class. If you also want ownership, put it in a unique_ptr. (shared_ptr is not a good fit.) Then, you have to implement copy operations, so put the unique_ptr inside a wrapper type and define copy operations. The classical approach is to define a method as part of the base class interface clone() which every derived class overrides to copy itself. The unique_ptr wrapper can call that method when it needs to copy.

That's a valid approach, although it has some tradeoffs. Requiring a base class is intrusive, and may be painful if you simultaneously want to satisfy multiple interfaces. std::vector<T> and std::set<T> do not share a common base class but both are iterable. Additionally, the clone() method is pure boilerplate.

Type erasure takes this one step more and removes the need for a common base class.

In this approach, you still define a base class, but for you, not your user:

struct Base {
  virtual ~Base() = default;
  virtual auto clone() const -> std::unique_ptr<Base> = 0;
  virtual auto name() const -> std::string = 0;
};

And you define an implementation that acts as a type-specific delegator. Again, this is for you, not your user:

template <typename T>
struct Impl: Base {
  T t;
  Impl(T &&t) : t(std::move(t)) {}
  auto clone() const -> std::unique_ptr<Base> override {
    return std::make_unique<Impl>(*this);
  }
  auto name() const -> std::string override {
    return t.name();
  }
};

And then you can define the type-erased type that the user interacts with:

class BaseLike
{
public:
  template <typename B>
  BaseLike(B &&b)
    requires((!std::is_same_v<std::decay_t<B>, BaseLike>) &&
             base_like<std::decay_t<B>>)
  : base(std::make_unique<detail::Impl<std::decay_t<B>>>(std::move(b))) {}

  BaseLike(const BaseLike& other) : base(other.base->clone()) {}

  BaseLike& operator=(const BaseLike& other) {
    if (this != &other) {
      base = other.base->clone();
    }
    return *this;
  }

  BaseLike(BaseLike&&) = default;

  BaseLike& operator=(BaseLike&&) = default;

  auto name() const -> std::string {
    return base->name();
  }

private:
  std::unique_ptr<Base> base;
};

In the end, this option looks like: https://godbolt.org/z/P3zT9nb5o

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文