【问题标题】:Virtual function efficiency and the 'final' keyword虚函数效率和“final”关键字
【发布时间】:2015-02-04 22:39:11
【问题描述】:

考虑一个程序,它有一个类 Foo,其中包含一个函数 Foo::fn,声明如下:

virtual void fn();

还有一个名为BarFoo 的子类。将像这样声明Bar::fn

virtual void fn() override final;

导致在BarBar 的子类中调用fn 更有效,还是它只会阻止Bar 的子类覆盖fn?如果使用final 使调用更高效,那么定义Bar::fn 使其功能与Foo::fn 完全相同的最简单、最有效的方法是什么?

【问题讨论】:

  • 没有。编译器在确定如何编译调用Foo::fn 的代码时甚至不知道Bar 的存在(假设最常见的情况是它位于另一个禁用了全程序优化的翻译单元中)。你为什么担心 vtable 的大小?
  • 我怀疑 vtable 的大小保持不变,但 final 标记了它,因此编译器不允许在派生类中覆盖
  • 类的实例中通常没有 vtable。实例包含 vptrs。
  • 一个更好的问题可能是询问何时可以忽略 vtable。例如。 void (Bar &b) { b.fn(); }。这可以忽略 vtable,因为它知道 b.fn() 必须调用 B::fn
  • 不,它不会使 vtable 变小。即使您已将其声明为 final,它仍然需要在 vtable 中,因为您可以通过基类调用它。

标签: c++ performance c++11 vtable


【解决方案1】:

如果fnBar 中定义为final,则编译器可以通过指针或对Bar 的引用静态地调度对fn 的调用,因为它知道Bar::fn 是最终的覆盖器。比如这个程序片段:

struct Foo {
  virtual void fn();
};

struct Bar : Foo {
  void fn() final override;
};

void with_foo(Foo& o) { o.fn(); }
void with_bar(Bar& o) { o.fn(); }

编译为 (See gcc.godbolt.org for details):

with_foo(Foo&):
    subq    $8, %rsp
    movq    (%rdi), %rax
    call    *(%rax)
    addq    $8, %rsp
    ret

with_bar(Bar&):
    subq    $8, %rsp
    call    Bar::fn()
    addq    $8, %rsp
    ret

with_foo 中的调用通过 vtable 动态调度(call *(%rax) 是间接调用),但with_bar 中的调用静态调度到Bar::fn()

使Bar::fn 成为Foo::fn 的最终覆盖而不改变行为的最简单方法是将其定义为静态调用Foo::fn

struct Bar : Foo {
  void fn() final override { Foo::fn(); }
};

【讨论】:

  • 我对汇编的了解有点少,但这似乎是我一直在寻找的……本质上,这表明当@987654339 的实例调用Bar::fn 时效率更高@存储为Bar,但存储为Foo时没有增益,对吗?
  • @Conduit 正确。通常,编译器无法静态确定 Foo*Foo& 实际上是指 Bar 实例。
  • 稍微考虑一下就说得通了。正是我当时正在寻找的东西 - 谢谢!
【解决方案2】:

我从来不关心 vtable 的大小。它通常相对较小,每个类声明只有一个。更麻烦的是类实例中占用的额外空间,因为除了单例之外,类实例通常很多。因此,以某种方式将额外的元素添加到类中肯定会影响内存量。如果 vtable 太大真的让您感到困扰,那么请进行一些重新设计,以便没有那么多不同的虚拟成员函数(可能将类层次结构分成几个类)或更少的派生类。但实际上,即使你有数百个类,每个类都有一百个虚拟成员函数,它仍然相对较小——200 个类和 100 个成员将占用每个条目 20000 * 8 字节 [64 位架构] -> 160KB。肯定有 20000 个函数 [是的,理论上,每个派生类只需要一个新函数就需要一个新的 vtable,但这是一个相当愚蠢的设计,实际上不太可能]

final 关键字的目的是确保您不会从它进一步派生 - 例如,如果您有一个不应“更改”某些特定功能的基本类层次结构,这很有用。比如说你有:

class user_base
{
    public:
      virtual bool check_password(); {... magical code .. };
      virtual bool is_super_user() = 0;
};

class superuser : public user_base
{
    public:
      virtual bool check_password() final  
        { .... magical code ...
          ... extra checks to ensure no remote login... 
        }
      virtual bool is_super_user() final { return true; }

 };

 class user : public user_base
 {
  public:
      virtual bool is_super_user() final { return false; }
 };

您必须采取一些技巧来确保user_base 不被用作fake_super_user 的基类,当然,这样的设计还有其他大的安全问题,但它给了你一些想法。

【讨论】:

  • 我认为这几乎涵盖了我一直在寻找的内容,假设我对您的说法是正确的,final 仅用于确保子类不能覆盖该函数,但不会影响编译后的结果。听起来我可能在滥用我的术语......我从你的回答中推断出 vtables 与班级保持一致。实际实例存储了什么?
  • 没关系 - 找到了我要找的东西。编辑后的问题。
  • 该类包含一个指向实例的 vtable(s) 的指针。 [在存在多重继承的情况下,您可以拥有多个 vtables]
猜你喜欢
  • 2019-08-21
  • 2011-10-26
  • 1970-01-01
  • 2010-10-22
  • 2018-05-17
  • 2011-07-24
  • 1970-01-01
  • 2015-07-28
  • 2014-01-30
相关资源
最近更新 更多