将派生 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;
};
现在您有一个列表,您可以在其中放置 Derivative1
和 Derivative2
并像处理任何 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
我想在主机 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;
};
现在您有一个列表,您可以在其中放置 Derivative1
和 Derivative2
并像处理任何 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