【问题标题】:Are inheritance costs relative to whether objects are allocated on the stack or heap?继承成本与对象是在堆栈还是堆上分配有关吗?
【发布时间】:2013-05-13 04:35:17
【问题描述】:

考虑以下设置。

class I
{
public:
    virtual void F() = 0;
};

class A : public I
{
public:
    void F() { /* some implementation */ }
};

class B : public I
{
public:
    void F() { /* some implementation */ }
};

这让我可以编写如下函数。

std::shared_ptr<I> make_I(bool x)
{
    if (x) return std::make_shared<A>();
    else return std::make_shared<B>();
}

在这种情况下,我为继承和多态性付出了一些代价,即拥有一个 vtable,并且在如下使用时不能内联对 F 的调用(如果我错了,请纠正我)。

auto i = make_I(false);
i->F(); //can't be inlined

我想知道的是,当使用AB 作为堆栈上分配的对象时,我是否必须支付同样的费用,如下面的代码所示。

A a;
a.F();

AB 在堆栈上分配时是否有 vtables?对F 的调用可以内联吗?

在我看来似乎编译器可以为继承层次结构中的类创建两种内存布局——一种用于堆栈,一种用于堆。这是 C++ 编译器会/可能会做的事情吗?或者是否有理论上或实践上的原因它不能?


编辑:

我看到一条评论(看起来好像被删除了)实际上提出了一个很好的观点。您总是可以执行以下操作,然后在堆栈上分配 A a 可能不是我想要达到的重点......

A a;
A* p = &a;
p->F(); //likely won't be inlined (correct me if I'm wrong)

也许更好的表达方式是“分配在堆栈上的对象的行为是否不同并用作'常规值类型'?”如果您知道我的意思但有更好的表达方式,请在这里帮助我解决术语问题!

我要说明的是,您可以在编译时将基类的定义“扁平化”到派生类中,以便在堆栈上分配一个实例。

【问题讨论】:

  • @DyP 这看起来肯定是相关的——谢谢,我会通读一遍。但我不确定它是否重复。区别在于,在这里我还对编译器是否可以根据使用情况改变类型的内存布局(和大小)等内容感兴趣。
  • @TimothyShields 班级的大小和布局总是一样的。
  • @brianbeuning 是的,你是对的 - 如果这不是真的,sizeof(A) 将如何工作?我认为 Scott Jones 的回答是我真正想要的。

标签: c++ inheritance


【解决方案1】:

我认为您的问题确实与编译器是否具有对象的静态知识并且可以省略 vtable 查找(您在编辑中提到这一点)有关,而不是对象所在的位置是否有区别 - 堆栈或堆。是的,在这种情况下,许多编译器可以省略虚拟调度。

【讨论】:

  • 只是想补充一点,无论是否使用,对象都将始终具有 vtable,除非该类非常简单且其生命周期如此短,以至于编译器可以完全忽略该对象。
  • 回顾了我正在研究的实际应用程序,然后查看了您的答案,我认为您是对的 - 它实际上只是省略了我真正感兴趣的 vtable 查找。跨度>
【解决方案2】:

对您问题的编辑询问您是否可以将基类A 的定义展平为派生类B。如果编译器可以在编译时判断一个对象将只包含B 的一个实例,那么它可以在运行时消除vtable 查找并为该特定调用调用B.F();

例如,编译器将可能在下面的运行时消除vtable查找并调用派生函数:

B b;
b.F();

在下面的代码中,编译器将无法消除doSomething 中的运行时查找,但可能可以消除b.F() 中的查找

void doSomething( A* object ) {
    object->F();   // will involve a vtable lookup
}

B b;
b.F();    // probably won't need a vtable lookup
doSomething( &b );

请注意,对象是在堆栈还是堆上分配并不重要。重要的是编译器能够确定类型。每个类仍然有一个 vtable,只是每个方法调用可能并不总是需要它。

您提到代码内联,这与对象的分配方式无关。当调用普通函数时,变量将连同返回地址一起被压入堆栈。然后 CPU 将跳转到该函数。使用内联代码,函数调用的位置被实际代码替换(类似于宏)。

如果继承层次结构中包含的对象在堆栈上分配,编译器仍然需要能够确定它可以调用哪些函数,尤其是在存在虚拟和非虚拟函数的情况下。

【讨论】:

  • 假设我有class A { ... }class B : public A { ... },然后我有代码B b; /* anything using b */。似乎我总是可以定义一个等于Bclass B2,如果它仅限于以“非多态”方式使用,而不管AB 的定义是什么...至少在理论上……也许我对此很天真。一个反例说明为什么该陈述不能成为一般情况,我几乎可以说服我,因为我已经怀疑它是否可能。这只是我很好奇的事情。
猜你喜欢
  • 1970-01-01
  • 2016-12-27
  • 1970-01-01
  • 1970-01-01
  • 2012-07-28
  • 2018-09-09
  • 2019-10-25
相关资源
最近更新 更多