我可以在固定大小的假设下拥有多态值的向量吗?

Can I have a vector of polymorphic values under a fixed sized assumption?

考虑下面的代码,派生的 class 将虚拟成员函数替换为函数的变体,但不添加任何新的成员变量。 Base 和 Derived 的值被添加到一个公共容器 std::vector 并且正如预期的那样 Derived 值被切片。但是,通过将 Derived 值在内存中的表示复制到容器中,该值实际上只是部分切片。

#include <iostream>
#include <vector>

class Base {
public:
    Base() = default;
    Base(float arg) : a{ arg } {};

    virtual float doSomething(float b) const { return a + b; }

    float a;
};

class Derived : public Base {
public:
    Derived() = default;
    Derived(float a) : Base{ a } {};

    float doSomething(float b) const { return a - b; }
};

int main()
{
    Base b{ 1.0f };
    Derived d{ 1.0f };

    std::cout << sizeof(b) << ", " << sizeof(d) << '\n';  // 8, 8

    std::vector<Base> v{ b, d };  // d is sliced
    std::cout << v[0].doSomething(2.0f) << '\n';  // 3
    std::cout << v[1].doSomething(2.0f) << '\n';  // 3 as d was sliced

    memcpy(&v[1], &d, sizeof(d));  // Copy the representation of d over to v[1]
    std::cout << v[1].doSomething(2.0f) << '\n';  // now -1
}

由于指向虚函数的指针,值的大小为 8 table,这就是上述多态性的实现方式。 v[1] 的类型始终是 Base,因此如果 Derived 添加了一个新的成员函数,将无法调用它。实际上 v[1] 仍然被切片到 Base 但具有 Derived.

的重新实现的成员函数

假设 Base 本质上是 POD 但添加了虚拟成员函数,所有这些都是常量,即内存可复制,并且 Derived 仅重新实现这些成员函数:

  1. 以上代码是否属于未定义行为?
  2. 如果是这样,有没有一种方法可以在没有 memcpy 或等效的情况下以定义行为的方式实现它?
  3. 如果这是一个常见的模式,它叫什么?

从您的问题开始:

  1. Does the above code fall into undefined behaviour?

是的。在 非平凡可复制的 对象上使用 memcpy 是未定义的行为。

  1. If so is there a way to implement this without the memcpy or equivalent in a way that would be defined behaviour?

是的,有。它仍然会使用多态性——不是针对您存储的对象,而是针对它的字段。

  1. If this is a common pattern, what is it called?

是的。建议的解决方案有一个名称。它被称为 Strategy-PatternState-Pattern (取决于您要达到的目的到底是什么)。

这是您尝试实现的等效代码(在某种程度上):

不同的策略

class Base {
public:
    virtual ~Base() {}
    virtual float doSomething(float a, float b) const { return a + b; }
};

class Derived : public Base {
public:
    float doSomething(float a, float b) const override { return a - b; }
};

要存储的实际类型

class RealType {
    float a;
    const Base* strategy;
public:
    // just for the example, could be implemented in other ways
    const static Base BaseStrategy;
    const static Derived DerivedStrategy;

    RealType(float val, const Base& s): a(val), strategy(&s) {}
    float doSomething(float b) const { return strategy->doSomething(a, b); }
};

const Base RealType::BaseStrategy {};
const Derived RealType::DerivedStrategy {};

使用示例

int main()
{
    RealType b{ 1.0f, RealType::BaseStrategy };
    RealType d{ 1.0f, RealType::DerivedStrategy };

    std::cout << sizeof(b) << ", " << sizeof(d) << '\n';  // size of pointer

    std::vector<RealType> v{ b, d };  // no slicing
    std::cout << v[0].doSomething(2.0f) << '\n';  // 3
    std::cout << v[1].doSomething(2.0f) << '\n';  // -1 as no slicing

    v[0] = v[1]; // copies both the value stored in v[1] as well as the strategy
    std::cout << v[0].doSomething(2.0f) << '\n';  // now -1 with v[0]
}

代码:http://coliru.stacked-crooked.com/a/e1b102bc70427177

Amir 的回答正是我需要的。但是,为了完整起见,我现在将填写我提出问题时真正想要的内容。

可以在 std::vector 中存储不同类型的值,只要这些类型本身适当地包装在一个通用类型中,本质上是一种类型擦除形式。容器内部将有一个固定长度的缓冲区,并具有适当的对齐方式,例如使用 std::aligned_storage。然后,您将使用 placement new 和具有克隆功能的每种类型的模板来实现 copy/move 语义。

一个明显的改进是允许 unique_ptr 容纳的缓冲区太大的类型。从这个角度来看,容器实际上是 unique_ptr 的包装器,其中包含一个小的缓冲区优化。

我打算将此作为自己的练习,我将编辑此答案if/when我有机会。