![](/img/trans.png)
[英]Best way to insert items from a Derived class's constructor into a Base class's private std::vector?
[英]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;
};
現在您有一個列表,您可以在其中放置Derivative1
和Derivative2
並像處理任何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.