簡體   English   中英

將派生 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 的每個可能實例......

對此有什么想法/建議嗎?

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並沒有提供太多價值。 憑借抽象方法,您知道所有派生類都正確地實現了它。 但是,在這種情況下,我們知道所有的派生類,如果它們無法實現該方法,那么訪問將無法編譯。 所以, 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。如果您還想要所有權,請將其放入unique_ptr中。 shared_ptr不太合適。)然后,您必須實現復制操作,因此將unique_ptr放在包裝類型中並定義復制操作。 經典方法是定義一個方法作為基 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;
};

並且您定義了一個充當特定類型委托者的實現。 同樣,這是給你的,而不是你的用戶:

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM