无需基于 vtable 的多态性即可实现运行时多态性

问题描述 投票:0回答:3

我正在经历基于 vtable 的多态性的缺点。

  1. Vtable Overhead:每个具有虚函数的类都需要有一个 vtable,其中包含指向其虚拟函数的函数指针。 这会增加对象的大小并消耗额外的 记忆力。

  2. 动态转换开销:使用dynamic_cast向下转换指针时 对于派生类,有额外的运行时开销 执行类型检查。

  3. 对象切片:将派生对象存储在基础容器中时 类对象,可能会发生对象切片。

  4. 指针的管理:防止对象切片,更好的管理 多态对象,我们必须使用指针,这可能需要 堆分配。

基于上述缺点,我想到了以下设计来实现运行时多态性。

class B; class C;

class A{
public:
    int common_data1; //common properties
    int common_data2;
    
    int type; //type of child stored by the following variant. e.g. 0 for B, 1 for C. we cal also use enum instead of int
    std::variant<B,C> child;
    void common();
    void default_behavior();
    void specific()
    {
        switch(type){
            case 0:
                std::get<B>(child).specific();
                break;
            case 1:
                std::get<C>(child).specific();
                break;
            default:
                default_behavior();
        }
    }
};

class B : public A{
public:
    int b_data;
    void b_fun();
    void specific();
};

class C : public B{
public:
    int c_data;
    void c_fun();
    void specific();
};

似乎消除了上述缺点。

  1. 没有 Vtable 开销,因为没有虚拟函数。
  2. 无需动态演员表。
  3. 没有对象切片,因为我们将子对象存储在
    std::variant
  4. 由于没有发生切片,所以我们不需要使用指针来实现 多态性。

尽管使用

std::variant
会带来额外的开销,但与前面提到的缺点相比,它似乎提供了更好的替代方案。 C++ 专家能否指导我进行此设计并提供他们的深刻见解?

c++ inheritance polymorphism
3个回答
2
投票
  1. vtable位于类中,而不是位于对象中。该对象存储某种对 vtable 的引用,可以是指针。
  2. 应避免动态投射。多态性是指调用将被分派到适当对象的成员函数。
  3. 是的,你必须意识到这一点
  4. 智能指针缓解了这个问题

vtable 方法的优点是它是标准的,C++ 程序员应该知道它,并且它避免了类中的

type
和 switch-cases 之类的事情。


1
投票

您当前的实现将无法编译,因为

std::variant
无法使用不完整的类型进行实例化。然而,一般来说,
std::variant
可以是基于值类型的替代基于继承的多态性。但它确实有它自己的问题。

如果你像这样编写代码,它应该可以编译。

#include <variant>

struct Common {
    int common_data1; // Common properties
    int common_data2;
    
    void common();
};

struct B: Common {
    int b_data;

    void specific();
};

struct C: Common {
    int c_data;

    void specific();
};

struct A: std::variant<B, C> {
    using Base = std::variant<B, C>;
   
    using Base::Base;
   
    void common() {  
        std::visit([](auto& self) { self.common(); }, *this);
    }

    void specific() {
        std::visit([](auto& self) { self.specific(); }, *this);
    }
};

与基于继承的多态性相比的缺点:

  • 它更像是样板文件,因为您需要将每个函数编写两次,一次在具体类中,一次在变体类中。
  • 不开放扩展。如果您想添加另一个实现,您总是需要修改
    A
  • 您失去了代码隔离性,因为
    A
    的所有用户都需要了解所有可能的实现(
    B
    C
    ...)来实例化
    variant

与基于继承的多态性相比的优点:

  • 您获得了值语义(没有任何对象切片的风险)。
  • 也许性能改进(如果可测量),因为您不需要动态分配内存,并且基于
    switch
    std::variant
    /
    std::visit
    的调度可能比通过隐藏在 vtable 指针后面的函数指针更快。

0
投票

你的方法基本上只是带有小胡子的vtables。每个类有一个 vtable。当调用虚函数时,我们会得到编译器秘密存储在代表实例运行时类型的类中的一些值。该值可能只是一个指针/数字,与您的示例完全相同。然后,我们通过查找与我们的运行时类型相对应的正确表(vtable)来“调度”该值。然后在表中查找正确的函数。就性能而言,这相当于大约 2 个指针间接寻址。你的 switch 语句基本上只是单个函数的 vtable。

您的方法具有类似的开销(内存和性能方面)。 “对象切片”也存在同样的问题,因为您通过将不同类型的事物存储在同一向量中来抽象出类型。唯一的好处是您不需要使用指针来实现多态性。

IMO 更好的方法是将不同类型的事物存储在不同的列表/向量中。不再有调度,不再有“对象切片”,不再有指针。它的缓存效率也更高,并且更易于阅读。

© www.soinside.com 2019 - 2024. All rights reserved.