在派生类中调用虚函数的最佳方法是什么,以便编译器可以内联或以其他方式优化调用?
示例:
class Base {
virtual void foo() = 0;
};
class Derived: public Base {
virtual void foo() {...}
void bar() {
foo();
}
};
我希望foo()
中的bar()
来电始终致电Derived::foo()
。
我的理解是,调用将导致vtable查找,并且编译器无法优化它,因为可能有另一个继承自Derived的类。
我可以显式调用Derived::foo()
但是如果Derived中有许多虚函数调用则会变得冗长。我也发现令人惊讶的是我找不到很多在线资料来解决我认为是常见的情况(一个'最终'派生类调用虚拟方法)所以我想知道我是在这里滥用虚函数还是过度优化。
应该怎么做?停止过早优化并坚持使用foo()
,吸收它并使用Derived::foo()
,还是有更好的方法?
答案 0 :(得分:6)
C ++ 11包含final
关键字,"指定无法在派生类中重写虚函数,或者无法继承类。" {{3 }}
如果已经声明了final
,那么g ++似乎能够优化派生类中的虚函数调用。
我创建了以下测试:
virtualFunctions.h
#pragma once
class Base {
public:
virtual void foo();
virtual void bar();
virtual void baz();
int fooVar, barVar, bazVar;
};
class Derived: public Base {
public:
void test();
virtual void foo();
virtual void bar();
virtual void baz() final;
};
virtualFunctions.cpp
:
#include "virtualFunctions.h"
void Derived::test() {
foo();
Derived::bar();
baz();
}
void Derived::foo() {
fooVar = 101;
}
void Derived::bar() {
barVar = 202;
}
void Derived::baz() {
bazVar = 303;
}
我正在使用g ++ 4.7.2和-O1生成的程序集包含:
_ZN7Derived4testEv:
.LFB0:
.loc 1 3 0
.cfi_startproc
.LVL3:
pushl %ebx
.LCFI0:
.cfi_def_cfa_offset 8
.cfi_offset 3, -8
subl $24, %esp
.LCFI1:
.cfi_def_cfa_offset 32
movl 32(%esp), %ebx ; Load vtable from the stack
.loc 1 4 0
movl (%ebx), %eax ; Load function pointer from vtable
movl %ebx, (%esp)
call *(%eax) ; Call the function pointer
.LVL4:
.loc 1 5 0
movl %ebx, (%esp)
call _ZN7Derived3barEv ; Direct call to Derived::bar()
.LVL5:
.loc 1 6 0
movl %ebx, (%esp)
call _ZN7Derived3bazEv ; Devirtualized call to Derived::baz()
Derived::bar()
和Derived::baz()
都是直接调用的,而vtable则用于foo()
。
答案 1 :(得分:3)
如果编译器可以静态地找出使用的类型,则编译器可以对其进行优化并执行虚拟化。
虚拟方法调用非常便宜。前段时间我读了一篇文章,说明与普通的方法调用相比,开销大约是10%。这当然不会考虑缺少内联机会。
我也感觉这混合了界面和实现。我认为最好将它拆分为纯接口和实现类。
答案 2 :(得分:2)
正如您自己所说,只有在极少数情况下,这应该是您关心的性能影响。
如果您正在编译为C ++ 11,则可以将Derived
和/或foo()/bar()
声明为final,编译器可能内联它。
答案 3 :(得分:0)
问题的答案是禁用动态调度,这可以通过限定来完成:
class Derived: public Base {
virtual void foo() {...}
void bar() {
Derived::foo(); // no dynamic dispatch
}
};
现在的问题是,这是否会对性能产生影响(在改变之前采取措施!)以及是否有意义这样做。虚函数是派生类型的扩展点。如果您停用动态调度,有人可能会创建MoreDerived
,实施foo
并期望bar
调用MoreDerived::foo
,但如果您禁用了动态调度,则不会发生。
除非有一个非常好的,有计划的,尝试微观优化的理由,否则完全避免这个问题。如果您在分析器中运行代码,动态调度根本不会显示出来。