将派生 class 的 std::vector 存储在宿主父 class 中的最佳方式

Best way to store std::vector of derived class in a host parent class

我想在主机 class 中存储一个 std::vector<>,其中包含具有公共基础 class 的对象。主机 class 应该保持可复制,因为它存储在其所有者 class.

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()。这比我不能与 std::shared_ptr<> 一起使用的简单 resize(N) 花费更多时间,因为它将为每个插槽创建相同的指针实例。

我考虑过使用 std::unique_ptr<>,但这不可行,因为它使 Host class 不可复制(std::vector 要求的功能)。

否则,我可以使用 std::variant<Derived1, Derived2> 来做我想做的事。但是我需要声明派生的每个可能实例 class...

对此有任何thought/advice吗?

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

现在您有一个列表,您可以在其中放置 Derivative1Derivative2 并像处理任何 Base&.

一样处理对元素的引用

现在有趣的是 Base 没有提供太多价值。凭借抽象方法,您知道所有派生的 classes 都正确地实现了它。但是,在这种情况下,我们知道所有派生的 classes,如果它们无法实现该方法,则访问将无法编译。所以,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

类型擦除

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

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

这是一个有效的方法,尽管它有一些权衡。要求基数 class 是侵入性的,如果您同时想要满足多个接口,可能会很痛苦。 std::vector<T>std::set<T> 不共享一个公共基础 class 但两者都是可迭代的。此外,clone() 方法是纯粹的样板。

类型擦除更进一步,消除了对公共基础的需求 class。

在这种方法中,您仍然定义了一个基础 class,但对您而言,而不是对您的用户:

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

并且您定义了一个充当 type-specific 委托人的实现。同样,这是给你的,而不是你的用户:

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

然后你可以定义用户交互的type-erased类型:

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