8

考虑以下设置。

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

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

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


编辑:

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

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

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

我试图理解的一点是,您可以在编译时将基类的定义“展平”到您在堆栈上分配实例的派生类中。

4

2 回答 2

7

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

于 2013-05-17T22:31:30.327 回答
1

对您问题的编辑询问您是否可以将基类的定义展平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 将跳转到该函数。使用内联代码,函数调用的位置被实际代码替换(类似于宏)。

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

于 2013-05-17T22:32:03.440 回答