编译器如何从C++的新final关键字中受益?

tow*_*owi 29 c++ compiler-construction final c++11

C++ 11将允许将类和虚方法标记为最终,以禁止从它们派生或覆盖它们.

class Driver {
  virtual void print() const;
};
class KeyboardDriver : public Driver {
  void print(int) const final;
};
class MouseDriver final : public Driver {
  void print(int) const;
};
class Data final {
  int values_;
};
Run Code Online (Sandbox Code Playgroud)

这非常有用,因为它告诉读者接口有关使用此类/方法的意图.如果用户尝试覆盖,则用户获得诊断也可能有用.

但编译器的观点是否有优势?当编译器知道"这个类永远不会从中派生出来"或"这个虚拟函数永远不会被覆盖"时,编译器能做些什么吗?

因为final我主要发现只有N2751指的是它.通过一些讨论,我发现了来自C++/CLI方面的论据,但没有明确暗示为什么final对编译器有用.我正在考虑这个问题,因为我也看到了标记类的一些缺点final:要对受保护的成员函数进行单元测试,可以派生一个类并插入测试代码.有时这些课程是很好的候选人final.在这些情况下,这种技术是不可能的.

Fle*_*exo 36

我可以想到一个从优化角度来看它可能对编译器有帮助的场景.我不确定编译器实现者是否值得努力,但理论上它至少是可行的.

通过virtual对派生final类型的调用分派,您可以确保没有其他任何内容派生自该类型.这意味着(至少在理论上)final关键字可以virtual在编译时正确地解析一些调用,这将使得一些优化成为可能,否则在virtual调用时是不可能的.

例如,如果你有delete most_derived_ptr,most_derived_ptr指向派生final类型的指针,则编译器可以简化对virtual析构函数的调用.

同样,对于virtual引用/指向最派生类型的成员函数的调用.

如果有任何编译器今天这样做,我会感到非常惊讶,但它似乎可能会在未来十年左右实现.

也可能有在能够推断出(在没有一些millage friendS)标记的东西protectedfinal class也有效地成为private.


Mat*_* M. 30

对函数的虚拟调用比正常调用稍微昂贵一些.除了实际执行调用之外,运行时必须首先确定要调用哪个函数,哪些函数会导致:

  1. 找到v表指针,并通过它到达v表
  2. 在v表中定位函数指针,并通过它执行调用

与直接调用(其中函数的地址事先已知(并且用符号硬编码))相比,这导致了小的开销.好的编译器设法使其比常规调用慢10%-15%,如果函数有任何关系,这通常是微不足道的.

编译器的优化器仍然试图避免各种开销,而函数函数调用通常是一个悬而未决的结果.例如,请参阅C++ 03:

struct Base { virtual ~Base(); };

struct Derived: Base { virtual ~Derived(); };

void foo() {
  Derived d; (void)d;
}
Run Code Online (Sandbox Code Playgroud)

Clang得到:

define void @foo()() {
  ; Allocate and initialize `d`
  %d = alloca i8**, align 8
  %tmpcast = bitcast i8*** %d to %struct.Derived*
  store i8** getelementptr inbounds ([4 x i8*]* @vtable for Derived, i64 0, i64 2), i8*** %d, align 8

  ; Call `d`'s destructor
  call void @Derived::~Derived()(%struct.Derived* %tmpcast)

  ret void
}
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,编译器已经足够智能,以确定d是一个Derived那么没有必要承担虚拟调用的开销.

实际上,它会很好地优化以下功能:

void bar() {
  Base* b = new Derived();
  delete b;
}
Run Code Online (Sandbox Code Playgroud)

但是在某些情况下编译器无法得出这样的结论:

Derived* newDerived();

void deleteDerived(Derived* d) { delete d; }
Run Code Online (Sandbox Code Playgroud)

在这里,我们可以(天真地)期望调用deleteDerived(newDerived());将导致与之前相同的代码.但事实并非如此:

define void @foobar()() {
  %1 = tail call %struct.Derived* @newDerived()()
  %2 = icmp eq %struct.Derived* %1, null
  br i1 %2, label %_Z13deleteDerivedP7Derived.exit, label %3

; <label>:3                                       ; preds = %0
  %4 = bitcast %struct.Derived* %1 to void (%struct.Derived*)***
  %5 = load void (%struct.Derived*)*** %4, align 8
  %6 = getelementptr inbounds void (%struct.Derived*)** %5, i64 1
  %7 = load void (%struct.Derived*)** %6, align 8
  tail call void %7(%struct.Derived* %1)
  br label %_Z13deleteDerivedP7Derived.exit

_Z13deleteDerivedP7Derived.exit:                  ; preds = %3, %0
  ret void
}
Run Code Online (Sandbox Code Playgroud)

公约可以规定newDerived返回a Derived,但编译器不能做出这样的假设:如果它返回了进一步派生的内容呢?这样的话你能看到所有的丑陋的机械参与检索v表指针,选择在表中相应的条目,并最终进行呼叫.

但是如果我们输入finalin,那么我们给编译器保证它不能是其他任何东西:

define void @deleteDerived2(Derived2*)(%struct.Derived2* %d) {
  %1 = icmp eq %struct.Derived2* %d, null
  br i1 %1, label %4, label %2

; <label>:2                                       ; preds = %0
  %3 = bitcast i8* %1 to %struct.Derived2*
  tail call void @Derived2::~Derived2()(%struct.Derived2* %3)
  br label %4

; <label>:4                                      ; preds = %2, %0
  ret void
}
Run Code Online (Sandbox Code Playgroud)

简而言之:final允许编译器在无法检测到相关函数的情况下避免虚拟调用的开销.

  • 那么,有*已经是一个编译器,通过应用他的优化魔法几乎可以直接从中受益?*Clang*几乎是最终准备好的? (2认同)
  • @towi:是的,可以内联.Clang中的虚拟化很早就会发生,因为它是在前端完成的优化(优化器不能识别OO).因此,一旦调用被虚拟化,所有可以在常规函数上完成的优化都是可能的.它们实际上是否真的发生取决于优化器是否认为它们是个好主意.关于第一个问题,我不认为Clang已经完全准备好了,尽管它已经在某些地方受益了.我实际上手工制作了最后一个虚拟化;) (2认同)