如果将对象强制转换为实际类型,是否需要使用虚函数?

Kev*_*sen 13 c++ performance inheritance vtable

我的理解是,由于两个问题,虚函数可能会导致性能问题:vtable引起的额外derefencing以及编译器无法在多态代码中内联函数.

如果我将变量指针向下转换为其确切类型怎么办?那还有额外的费用吗?

class Base { virtual void foo() = 0; };
class Derived : public Base { void foo() { /* code */} };

int main() {
    Base * pbase = new Derived();
    pbase->foo(); // Can't inline this and have to go through vtable
    Derived * pderived = dynamic_cast<Derived *>(pbase);
    pderived->foo(); // Are there any costs due to the virtual method here?
}
Run Code Online (Sandbox Code Playgroud)

我的直觉告诉我,由于我将对象转换为其实际类型,编译器应该能够避免使用虚函数的缺点(例如,它应该能够内联方法调用,如果它想要).它是否正确?

在我转发后,编译器是否真的知道pderived是Derived类型的?在上面的例子中,看到pbase是Derived类型的微不足道,但在实际代码中它可能在编译时是未知的.

既然我已经写下来了,我想由于Derived类本身可以被另一个类继承,将pbase向下转换为Derived指针实际上并不能确保编译器的任何内容,因此它无法避免成本虚拟功能?

Pra*_*han 21

神秘的Sufficiently Smart Compiler可以做什么,以及实际编译器最终做什么之间总是存在差距.在您的示例中,由于没有任何继承Derived,最新的编译器可能会将调用虚拟化为foo.但是,由于成功的虚拟化和后续内联通常是一个难题,因此尽可能使用final关键字帮助编译器.

class Derived : public Base { void foo() final { /* code */} }
Run Code Online (Sandbox Code Playgroud)

现在,编译器知道,只有一个可能foo,一个Derived*可以调用.

(有关为什么虚拟化很难以及gcc4.9 +如何处理它的深入讨论,请阅读Jan Hubicka在C++系列文章中的Devirtualization.)


Ton*_*roy 5

Pradhan的使用建议final是合理的,如果更改Derived类是一个选项,你不需要任何进一步的推导.

特定呼叫站点可直接使用的另一个选项是为函数名称添加前缀Derived::,禁止虚拟调度到任何进一步的覆盖:

#include <iostream>

struct Base { virtual ~Base() { } virtual void foo() = 0; };

struct Derived : public Base
{
    void foo() override { std::cout << "Derived\n"; }
};

struct FurtherDerived : public Derived
{
    void foo() override { std::cout << "FurtherDerived\n"; }
};

int main()
{
    Base* pbase = new FurtherDerived();
    pbase->foo(); // Can't inline this and have to go through vtable
    if (Derived* pderived = dynamic_cast<Derived *>(pbase))
    {
        pderived->foo();  // still dispatched to FurtherDerived
        pderived->Derived::foo();  // static dispatch to Derived
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

FurtherDerived
FurtherDerived
Derived
Run Code Online (Sandbox Code Playgroud)

这可能很危险:实际的运行时类型可能取决于调用它的覆盖以维持其不变量,因此除非存在紧迫的性能问题,否则使用它是个坏主意.

代码在这里可用.