我可以在固定大小的假设下拥有多态值的向量吗?
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 仅重新实现这些成员函数:
- 以上代码是否属于未定义行为?
- 如果是这样,有没有一种方法可以在没有 memcpy 或等效的情况下以定义行为的方式实现它?
- 如果这是一个常见的模式,它叫什么?
从您的问题开始:
- Does the above code fall into undefined behaviour?
是的。在 非平凡可复制的 对象上使用 memcpy
是未定义的行为。
- If so is there a way to implement this without the memcpy or equivalent in a way that would be defined behaviour?
是的,有。它仍然会使用多态性——不是针对您存储的对象,而是针对它的字段。
- If this is a common pattern, what is it
called?
是的。建议的解决方案有一个名称。它被称为 Strategy-Pattern 或 State-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]
}
Amir 的回答正是我需要的。但是,为了完整起见,我现在将填写我提出问题时真正想要的内容。
可以在 std::vector 中存储不同类型的值,只要这些类型本身适当地包装在一个通用类型中,本质上是一种类型擦除形式。容器内部将有一个固定长度的缓冲区,并具有适当的对齐方式,例如使用 std::aligned_storage。然后,您将使用 placement new 和具有克隆功能的每种类型的模板来实现 copy/move 语义。
一个明显的改进是允许 unique_ptr 容纳的缓冲区太大的类型。从这个角度来看,容器实际上是 unique_ptr 的包装器,其中包含一个小的缓冲区优化。
我打算将此作为自己的练习,我将编辑此答案if/when我有机会。
考虑下面的代码,派生的 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 仅重新实现这些成员函数:
- 以上代码是否属于未定义行为?
- 如果是这样,有没有一种方法可以在没有 memcpy 或等效的情况下以定义行为的方式实现它?
- 如果这是一个常见的模式,它叫什么?
从您的问题开始:
- Does the above code fall into undefined behaviour?
是的。在 非平凡可复制的 对象上使用 memcpy
是未定义的行为。
- If so is there a way to implement this without the memcpy or equivalent in a way that would be defined behaviour?
是的,有。它仍然会使用多态性——不是针对您存储的对象,而是针对它的字段。
- If this is a common pattern, what is it called?
是的。建议的解决方案有一个名称。它被称为 Strategy-Pattern 或 State-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]
}
Amir 的回答正是我需要的。但是,为了完整起见,我现在将填写我提出问题时真正想要的内容。
可以在 std::vector 中存储不同类型的值,只要这些类型本身适当地包装在一个通用类型中,本质上是一种类型擦除形式。容器内部将有一个固定长度的缓冲区,并具有适当的对齐方式,例如使用 std::aligned_storage。然后,您将使用 placement new 和具有克隆功能的每种类型的模板来实现 copy/move 语义。
一个明显的改进是允许 unique_ptr 容纳的缓冲区太大的类型。从这个角度来看,容器实际上是 unique_ptr 的包装器,其中包含一个小的缓冲区优化。
我打算将此作为自己的练习,我将编辑此答案if/when我有机会。