是相对于对象是在堆栈上还是在堆上分配的继承成本

Are inheritance costs relative to whether objects are allocated on the stack or heap?

本文关键字:继承 分配 相对于 对象 堆栈      更新时间:2023-10-16

考虑以下设置:

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

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

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

我想知道的是,当我使用AB作为堆栈上分配的对象时,是否必须支付相同的成本,就像下面的代码一样。

A a;
a.F();

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

对我来说似乎编译器可以为继承层次结构中的类创建两种内存布局——一种用于堆栈,一种用于堆。这是c++编译器将要/可能做的吗?还是存在理论上或实践上的原因?


编辑:

我看到了一条评论(看起来好像被删除了),它实际上提出了一个很好的观点。你总是可以这样做,然后A a被分配到堆栈上可能不是我想要得到的突出点…

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

也许更好的表达方式是"在堆栈上分配并用作'常规值类型'的对象的行为是否不同?"请帮助我这里的术语,如果你知道我的意思,但有一个更好的方式来把它!

我想说的是,你可以在编译时将基类的定义"扁平化"到你在堆栈上分配实例的派生类中。

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

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

例如,编译器将可能在下面的运行时消除虚函数表查找,并调用派生函数:
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 );

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

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

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