编译器如何从C++的新 final 关键字中受益?
How does the compiler benefit from C++'s new final keyword?
c++ 11将允许将类和虚方法标记为final,以禁止从它们派生或重写它们。
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_;
};
这是非常有用的,因为它告诉接口的读者使用这个类/方法的意图。如果用户试图重写,则会得到诊断信息,这可能也很有用。
但是从编译器的角度来看有什么优势吗?当编译器知道"这个类永远不会被派生"或"这个虚函数永远不会被覆盖"时,编译器可以做任何不同的事情吗?
对于final
,我主要发现只有N2751引用了它。通过对一些讨论的筛选,我发现了来自c++/CLI方面的争论,但没有明确提示为什么final
可能对编译器有用。我正在考虑这个问题,因为我也看到了标记类final
的一些缺点:要对受保护的成员函数进行单元测试,可以派生类并插入测试代码。有时,这些课程很适合用final
标记。这种技术在这些情况下是不可能的。
我可以想到一个场景,从优化的角度来看,它可能对编译器有帮助。我不确定编译器实现者是否值得付出努力,但至少在理论上是可能的。
对于派生的final
类型的virtual
调用调度,您可以确保没有从该类型派生的其他类型。这意味着(至少在理论上)final
关键字可以在编译时正确解析一些virtual
调用,这将使virtual
调用无法实现的许多优化成为可能。
例如,如果您有delete most_derived_ptr
,其中most_derived_ptr
是指向派生final
类型的指针,那么编译器可以简化对virtual
析构函数的调用。
对于virtual
成员函数在指向最派生类型的引用/指针上的调用也是如此。
如果今天有任何编译器这样做,我会感到非常惊讶,但这似乎是未来十年左右可能实现的事情。
可以推断出(在没有friend
s的情况下)在final
class
中标记为protected
的东西也有效地变成了private
。
虚函数调用比正常调用稍微昂贵一些。除了实际执行调用之外,运行时必须首先确定要调用哪个函数,这通常会导致:
- 定位v表指针,并通过它到达v表
- 在v表中定位函数指针,并通过它执行调用
与直接调用相比,在直接调用中,函数的地址是预先知道的(并且用符号硬编码),这会导致很小的开销。好的编译器会设法使它只比常规调用慢10%-15%,如果函数有任何内容,这通常是微不足道的。
编译器的优化器仍然寻求避免各种开销,而去虚拟化函数调用通常是容易实现的。例如,参见c++ 03:
struct Base { virtual ~Base(); };
struct Derived: Base { virtual ~Derived(); };
void foo() {
Derived d; (void)d;
}
叮当声得到:
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
}
正如你所看到的,编译器已经足够聪明地判断出d
是Derived
,那么就没有必要引起虚调用的开销。
实际上,它可以很好地优化以下函数:
void bar() {
Base* b = new Derived();
delete b;
}
然而,在某些情况下,编译器无法得出这个结论:
Derived* newDerived();
void deleteDerived(Derived* d) { delete d; }
在这里,我们可以(天真地)期望调用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
}
约定可以规定newDerived
返回Derived
,但是编译器不能做这样的假设:如果它返回的是进一步派生的东西呢?因此,您可以看到检索v-table指针、在表中选择适当的项并最终执行调用所涉及的所有丑陋的机制。
然而,如果我们把final
放进去,那么我们给编译器一个保证,它不能是其他任何东西:
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
}
简而言之:final
允许编译器在无法检测到的情况下避免相关函数的虚调用的开销。
取决于您如何看待它,编译器还有一个进一步的好处(尽管这个好处只是回报给用户,所以可以说这不是编译器的好处):编译器可以避免对具有不确定行为的结构发出警告,这些结构是可重写的。
例如,考虑以下代码:class Base
{
public:
virtual void foo() { }
Base() { }
~Base();
};
void destroy(Base* b)
{
delete b;
}
当观察到delete b
时,许多编译器会对b
的非虚析构函数发出警告。如果从Base
继承的类Derived
有自己的~Derived
析构函数,在动态分配的Derived
实例上使用destroy
通常会调用~Base
,但不会调用~Derived
。因此,~Derived
的清理操作不会发生,这可能很糟糕(尽管在大多数情况下可能不是灾难性的)。
如果编译器知道Base
不能继承,那么~Base
是非虚的就没有问题,因为不会意外跳过派生清理。将final
添加到class Base
中,可以为编译器提供不发出警告的信息。
我知道这样使用final
会抑制Clang的警告。我不知道其他编译器是否在这里发出警告,或者在确定是否发出警告时是否考虑了最终性。
- Visual Studio 2015:Extern "C" 和 "export" 关键字
- C++中的"inline"关键字
- 如何确保C++函数在定义之前声明(如override关键字)
- 谷歌模拟和覆盖关键字
- 结构体 S { int align; } 之间的区别;(struct 关键字后的名称)和 struct { int al
- 如果全局变量默认是外部变量,为什么要添加"extern"关键字?
- 当我从下面的代码中删除关键字 virtual 时,它可以正常工作,否则会出现错误。在这里"virtual"字的意义是什么?
- 为什么"delete"关键字不删除节点?
- 在 c++ 中正确定义"this"关键字?
- 是否有技术原因阻止 Java 中的 final C++ 像 const 一样严格?
- 这个额外的关键字在这个 c++ 类声明中是什么意思?
- 在 typedef 内部使用 const 关键字和在 typedef 外部使用 const 关键字之间有区别吗?
- 如果所有派生类在编译时都是已知的,那么final关键字是否提供了优化
- 将 final 关键字添加到没有基类(未派生)的类中的虚函数是否有意义
- c++ 中的 final 关键字是否允许额外的编译器优化
- C++11 中的 "final" 关键字对函数有什么用?
- java中的final关键字在应用时是否显示不同的效果:- 1)对原语2)对对象
- 编译器如何从C++的新 final 关键字中受益?
- QtCreator:如何将"override"和"final"注册为关键字?
- 我可以将final关键字应用于c++ 11中的POD(标准布局)结构体吗?我应该