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



我想在宿主类中存储一个包含具有公共基类的对象的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()。这比一个简单的resize(N)要花更多的时间,我不能用std::shared_ptr<>,因为它会为每个插槽创建相同的指针实例。

我想使用std::unique_ptr<>代替,但这是不可行的,因为它使Host类不可复制(std::vector要求的功能)。

否则,我可以使用std::variant<Derived1, Derived2>,它可以做我想做的。然而,我需要声明派生类的每个可能的实例…

对此有什么想法/建议吗?

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

类型擦除

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

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

这是一个有效的方法,尽管它有一些权衡。需要一个基类是侵入性的,如果同时想要满足多个接口,可能会很痛苦。std::vector<T>std::set<T>不共享公共基类,但它们都是可迭代的。此外,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

最新更新