连续存储多态类型

时间:2017-10-09 04:48:58

标签: c++ polymorphism virtual-method

我很想知道是否有任何可行的方法来连续存储多态对象数组,这样可以合法调用公共基础上的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(),它将根据对象类型委托给适当的方法。从概念上讲,这似乎是可能的:每个对象都知道其类型,通常是通过嵌入式 vtable 指针,所以你可以想象,将 n 对象存储在一个数组中n * max(sizeof(D1), sizeof(D2)) unsigned char,并使用展示位置newdelete初始化对象,并将unsigned char指针转换为B*。不过,我很确定演员表不合法。

人们还可以想象创建一个像:

这样的联盟
union Both {
  D1 d1;
  D2 d2;
}

然后创建一个Both数组,并使用placement new来创建相应类型的对象。然而,这似乎并没有提供一种实际安全地呼叫B::getVirtual()的方法。您不知道元素的最后存储类型,那么您将如何获得B*?您需要使用 &u.d1&u.d2,但您不知道哪个!实际上存在关于“初始公共子序列”的特殊规则,这些规则允许您在元素共享某些共同特征的联合上做一些事情,但是only applies to standard layout types。具有虚拟方法的类不是标准布局类型。

有没有办法继续?理想情况下,解决方案看起来像非切片std::vector<B>,实际上可以包含B的多态子类。是的,如果需要,可以预先确定所有可能的子类是已知的,但是更好的解决方案只需要知道任何子类的最大可能大小(如果有人试图添加“太大”的对象,则在编译时失败)

如果无法使用内置的virtual机制,那么提供类似功能的其他替代方案也会很有趣。

背景

毫无疑问,有人会问“为什么”,所以这里有一点动机:

一般众所周知,在实际调用虚方法时,使用virtual函数实现运行时多态性的时间为moderate overhead

然而,并不是经常讨论的事实是,使用具有虚方法的类来实现多态性通常意味着管理底层对象的内存的完全不同的方式。您不能只将不同类型的对象(但是共同的基础)添加到标准容器中:如果您有子类D1D2,则它们都来自基础B,{{1}将对任何添加的std::vector<B>D1个对象进行切片。类似地,对于这些对象的数组。

通常的解决方案是使用指针的容器或数组到基类,如D2std::vector<B*>std::vector<unique_ptr<B>>。至少,这在访问每个元素 1 时会增加额外的间接性,而在智能指针的情况下,它会中断common container optimizations。如果您实际上是通过std::vector<shared_ptr<B>>new(包括间接)分配每个对象,那么存储对象的时间和内存成本只会增加很多。

从概念上讲,似乎可以连续存储公共库的各种子类(每个对象将消耗相同数量的空间:最大支持对象的空间),并且指向对象的指针可以被视为基础 - 类指针。在某些情况下,这可以非常简单地加速使用这种多态对象。当然,总的来说,这可能是一个糟糕的想法,但出于这个问题的目的,让我们假设它有一些利基应用。

1 除此之外,这种间接性几乎可以防止应用于所有元素的相同操作的任何矢量化,并且会损害引用的局部性,同时影响缓存和预取。

5 个答案:

答案 0 :(得分:5)

你几乎和你的工会在一起。您可以使用带标记的联合(添加if来区分循环)或std::variant(它引入了一种通过std::find进行双重调度以从中获取对象)要做到这一点。在这两种情况下,您都没有对动态存储进行分配,因此可以保证数据的位置 无论如何,正如您所看到的,在任何情况下,您都可以使用普通直接调用替换额外级别的间接(虚拟调用)。你需要以某种方式擦除类型(多态性只不过是一种类型的擦除,想一想)而你无法直接从一个带有简单的擦除对象中获取呼叫。 <{1}}或额外的电话需要填补间接额外间隙的空白。

以下是使用ifstd::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);
}

答案 1 :(得分:4)

我试着在没有内存开销的情况下实现你想要的东西:

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;
};

Demo

但问题是复制/移动构造函数(和赋值)是不正确的,但我没有看到正确的方法来实现它而没有内存开销(或对类的额外限制)。

我不能=delete他们,否则你不能在std::vector中使用它们。

由于内存开销,variant似乎更简单。

答案 2 :(得分:3)

所以,这真的很难看,但是如果你不使用多重继承或虚拟继承,大多数实现中的Derived *将具有与{{1}相同的位级值。 }。

您可以使用Base *对此进行测试,因此如果特定平台上的情况并非如此,则无法编译,并使用您的static_assert提示。

union

唯一的问题是constexpr规则会禁止您的对象拥有虚拟析构函数,因为这些将被视为“非平凡的”&#39;。您必须通过调用必须在每个派生类中定义的虚函数来销毁它们,然后直接调用析构函数。

但是,如果这段代码成功编译,那么如果你从你工会的#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(); Base *成员那里得到DerivedA,那就无所谓了。无论如何,他们将会是一样的。

另一种选择是在结构的开头嵌入一个指向完整成员函数指针的结构的指针,该结构包含该指针和与其派生类的并集,并自己初始化它。基本上,实现自己的vtable。

答案 3 :(得分:3)

在CppCon 2017上有一个演讲,“Runtime Polymorphism - Back to the Basics”,讨论了做你想要的事情。 The slides位于github上,该演讲视频为available on youtube

演讲者的实验库实现了这个目标,“dyno”也是on github

答案 4 :(得分:1)

在我看来,您正在寻找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());
}