我很想知道是否有任何可行的方法来连续存储多态对象数组,这样 virtual
公共基础上的方法可以合法调用(并将调度到子类中正确的重写方法)。
例如,考虑以下类:
struct B {
int common;
int getCommon() { return common; }
virtual int getVirtual() const = 0;
}
struct D1 : B {
virtual int getVirtual final const { return 5 };
}
struct D2 : B {
int d2int;
virtual int getVirtual final const { return d2int };
}
我想分配一个连续的D1和D2对象数组,并将它们视为 B
对象,包括呼叫 getVirtual()
它将根据对象类型委托给适当的方法。从概念上讲这似乎是可能的:每个对象 知道 它的类型,通常通过嵌入式 虚函数表 指针,所以你可以想象,存储 ñ 数组中的对象 n * max(sizeof(D1), sizeof(D2))
unsigned char
,并使用展示位置 new
和 delete
初始化对象,并铸造 unsigned char
指向 B*
。不过,我很确定演员不合法。
人们还可以想象创建一个联盟,如:
union Both {
D1 d1;
D2 d2;
}
然后创建一个数组 Both
,并使用placement new来创建相应类型的对象。这似乎并没有提供实际调用的方法 B::getVirtual()
然而,安全。你不知道元素的最后存储类型,那么你将如何得到你的 B*
?你需要使用 或 &u.d1
要么 &u.d2
但你不知道哪个!实际上有关于“初始公共子序列”的特殊规则,它允许你在联盟中做一些事情,其中元素共享一些共同的特征,但是这个 仅适用于标准布局类型。具有虚拟方法的类不是标准布局类型。
有没有办法继续?理想情况下,解决方案看起来像非切片 std::vector<B>
实际上可以包含多态的子类 B
。是的,如果需要,可以预先确定所有可能的子类是已知的,但是更好的解决方案只需要知道任何子类的最大可能大小(如果有人试图添加“太大”的对象,则在编译时失败) 。
如果无法使用内置功能 virtual
机制,提供类似功能的其他替代方案也很有趣。
背景
毫无疑问,有人会问“为什么”,所以这里有一点动机:
使用起来似乎众所周知 virtual
实现运行时多态性的函数来自于 适度开销 当实际调用虚方法时。
然而,并不是经常讨论的事实是,使用具有虚方法的类来实现多态性通常意味着管理底层对象的内存的完全不同的方式。您不能只将不同类型的对象(但是一个共同的基础)添加到标准容器中:如果您有子类 D1
和 D2
,都来自基地 B
, 一个 std::vector<B>
会切片 D1
要么 D2
添加的对象。类似地,对于这种对象的数组。
通常的解决方案是使用容器或数组 指针 像基类一样 std::vector<B*>
也许 std::vector<unique_ptr<B>>
要么 std::vector<shared_ptr<B>>
。至少,这在访问每个元素时增加了额外的间接1,在智能指针的情况下,它会中断 常见的容器优化。如果你实际上是通过分配每个对象 new
和 delete
(包括间接),然后存储对象的时间和内存成本增加了很多。
从概念上讲,似乎可以连续存储公共库的各种子类(每个对象将占用相同数量的空间:最大支持对象的空间),并且指向对象的指针可以被视为基类指针。在某些情况下,这可以非常简单地加速使用这种多态对象。当然,总的来说,这可能是一个糟糕的想法,但出于这个问题的目的,让我们假设它有一些利基应用。
1 除此之外,这种间接性几乎可以防止应用于所有元素的相同操作的任何矢量化并损害引用的局部性,同时影响缓存和预取。
你几乎和你的工会在一起。您可以使用标记的联合(添加 if
在你的循环中区分)或a std::variant
(它引入了一种双重调度方式 std::find
从中获取对象)这样做。在这两种情况下,您都没有对动态存储进行分配,因此可以保证数据的位置。
无论如何,正如您所看到的,在任何情况下,您都可以使用普通直接调用替换额外级别的间接(虚拟调用)。你需要 抹去 某种类型的类型(多态性只不过是一种类型的擦除,想到它),你不能直接从一个擦除的对象中获取 简单 呼叫。 if
需要额外的电话来填补额外的间接水平。
这是一个使用的例子 std::variant
和 std::find
:
#include<vector>
#include<variant>
struct B { virtual void f() = 0; };
struct D1: B { void f() override {} };
struct D2: B { void f() override {} };
void f(std::vector<std::variant<D1, D2>> &vec) {
for(auto &&v: vec) {
std::visit([](B &b) { b.f(); }, v);
}
}
int main() {
std::vector<std::variant<D1, D2>> vec;
vec.push_back(D1{});
vec.push_back(D2{});
f(vec);
}
因为它非常接近,所以不值得发布使用标记联合的示例。
另一种方法是通过派生类的单独向量和支持向量以正确的顺序迭代它们。
这是一个显示它的最小示例:
#include<vector>
#include<functional>
struct B { virtual void f() = 0; };
struct D1: B { void f() override {} };
struct D2: B { void f() override {} };
void f(std::vector<std::reference_wrapper<B>> &vec) {
for(auto &w: vec) {
w.get().f();
}
}
int main() {
std::vector<std::reference_wrapper<B>> vec;
std::vector<D1> d1;
std::vector<D2> d2;
d1.push_back({});
vec.push_back(d1.back());
d2.push_back({});
vec.push_back(d2.back());
f(vec);
}
我试着在没有内存开销的情况下实现你想要的东西
template <typename Base, std::size_t MaxSize, std::size_t MaxAlignment>
struct PolymorphicStorage
{
public:
template <typename D, typename ...Ts>
D* emplace(Ts&&... args)
{
static_assert(std::is_base_of<Base, D>::value, "Type should inherit from Base");
auto* d = new (&buffer) D(std::forward<Ts>(args)...);
assert(&buffer == reinterpret_cast<void*>(static_cast<Base*>(d)));
return d;
}
void destroy() { get().~Base(); }
const Base& get() const { return *reinterpret_cast<const Base*>(&buffer); }
Base& get() { return *reinterpret_cast<Base*>(&buffer); }
private:
std::aligned_storage_t<MaxSize, MaxAlignment> buffer;
};
演示
但问题是复制/移动构造函数(和赋值)是不正确的,但我没有看到正确的方法来实现它没有内存开销(或对类的额外限制)。
我不能 =delete
他们,否则你不能使用它们 std::vector
。
内存开销, variant
似乎更简单。
所以,这真的很难看,但是如果你没有使用多重继承或虚拟继承,那么 Derived *
在大多数实现中,将具有与a相同的位级值 Base *
。
你可以测试一下 static_assert
因此,如果在特定平台上不是这种情况,则无法编译,并使用您的 union
理念。
#include <cstdint>
class Base {
public:
virtual bool my_virtual_func() {
return true;
}
};
class DerivedA : public Base {
};
class DerivedB : public Base {
};
namespace { // Anonymous namespace to hide all these pointless names.
constexpr DerivedA a;
constexpr const Base *bpa = &a;
constexpr DerivedB b;
constexpr const Base *bpb = &b;
constexpr bool test_my_hack()
{
using ::std::uintptr_t;
{
const uintptr_t dpi = reinterpret_cast<uintptr_t>(&a);
const uintptr_t bpi = reinterpret_cast<uintptr_t>(bpa);
static_assert(dpi == bpi, "Base * and Derived * !=");
}
{
const uintptr_t dpi = reinterpret_cast<uintptr_t>(&b);
const uintptr_t bpi = reinterpret_cast<uintptr_t>(bpb);
static_assert(dpi == bpi, "Base * and Derived * !=");
}
// etc...
return true;
}
}
const bool will_the_hack_work = test_my_hack();
唯一的问题是constexpr规则将禁止您的对象拥有虚拟析构函数,因为这些将被视为“非平凡”。您必须通过调用必须在每个派生类中定义的虚函数来销毁它们,然后直接调用析构函数。
但是,如果这段代码成功编译,那么如果你得到一个就没关系 Base *
来自 DerivedA
要么 DerivedB
你工会的成员。无论如何,它们将是相同的。
另一种选择是在结构的开头嵌入一个指向充满成员函数指针的结构的指针,该结构包含该指针以及与其中派生类的并集,并自己初始化它。基本上,实现自己的vtable。
在CppCon 2017上有一个演讲,“运行时多态性 - 回归基础“,那讨论了做你喜欢的事情。 幻灯片 在github和谈话的视频是 在youtube上可用。
演讲者的 试验 实现这一目标的库,“dyno”,也是 在github上。
在我看来,你正在寻找一个 variant
,这是一个安全访问的标记联盟。
c ++ 17有 std::variant
。对于以前的版本,boost提供了一个版本 - boost::variant
请注意,不再需要多态性。在这种情况下,我使用了与签名兼容的方法来提供多态性,但您也可以通过兼容签名的自由函数和ADL来提供它。
#include <variant> // use boost::variant if you don't have c++17
#include <vector>
#include <algorithm>
struct B {
int common;
int getCommon() const { return common; }
};
struct D1 : B {
int getVirtual() const { return 5; }
};
struct D2 : B {
int d2int;
int getVirtual() const { return d2int; }
};
struct d_like
{
using storage_type = std::variant<D1, D2>;
int get() const {
return std::visit([](auto&& b)
{
return b.getVirtual();
}, store_);
}
int common() const {
return std::visit([](auto&& b)
{
return b.getCommon();
}, store_);
};
storage_type store_;
};
bool operator <(const d_like& l, const d_like& r)
{
return l.get() < r.get();
}
struct by_common
{
bool operator ()(const d_like& l, const d_like& r) const
{
return l.common() < r.common();
}
};
int main()
{
std::vector<d_like> vec;
std::sort(begin(vec), end(vec));
std::sort(begin(vec), end(vec), by_common());
}