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_;
};
这非常有用,因为它告诉读者接口有关使用此类/方法的意图。用户在尝试覆盖时获得诊断也可能有用。
但是从编译器的角度来看是否有优势?当编译器知道&#34时,编译器可以做任何不同的事情吗?这个类永远不会从"或"永远不会覆盖此虚拟函数"?
对于final
我主要发现只有N2751参考它。通过一些讨论,我发现了来自C ++ / CLI方面的论据,但没有明确暗示为什么final
可能对编译器有用。我正在考虑这个问题,因为我也看到了标记类final
的一些缺点:要对受保护的成员函数进行单元测试,可以派生一个类并插入测试代码。有时这些课程是用final
标记的好人选。在这些情况下,这种技术是不可能的。
答案 0 :(得分:36)
我可以想到一个场景,从优化角度来看它可能对编译器有帮助。我不确定是否值得为编译器实现者付出努力,但理论上它至少是可能的。
对派生的virtual
类型进行final
调用分派,您可以确定没有其他内容来自该类型。这意味着(至少在理论上)final
关键字可以在编译时正确地解析一些virtual
调用,这将使得{{1}上无法实现的一些优化成为可能。调用。
例如,如果您有virtual
,其中delete most_derived_ptr
是指向派生的most_derived_ptr
类型的指针,那么编译器可以简化对final
的调用析构函数。
同样,对于引用/指向最派生类型的virtual
成员函数的调用。
如果有任何编译器今天这样做,我会感到非常惊讶,但它似乎可能在未来十年左右实现。
在virtual
friend
中,标记为protected
的内容也可能有效地变为{{}} {1}}。
答案 1 :(得分:30)
对函数的虚拟调用比普通调用稍微贵一些。除了实际执行调用之外,运行时必须首先确定要调用哪个函数,哪些函数会导致:
与预先知道函数地址(并用符号硬编码)的直接调用相比,这会导致较小的开销。好的编译器设法使它比常规调用慢10%-15%,如果函数有任何肉,这通常是微不足道的。
编译器的优化器仍然试图避免各种开销,而 devirtualizing 函数调用通常是一个悬而未决的结果。例如,请参阅C ++ 03:
struct Base { virtual ~Base(); };
struct Derived: Base { virtual ~Derived(); };
void foo() {
Derived d; (void)d;
}
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
}
正如您所看到的,编译器已足够聪明,可以确定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表指针所涉及的所有丑陋机器,在表中选择适当的条目并最终执行调用。
但是,如果我们放入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
允许编译器在无法检测到相关函数的情况下避免虚拟调用的开销。
答案 2 :(得分:0)
根据你的看法,编译器还有一个好处(尽管这个好处只会给用户带来好处,所以可以说这不是编译器的好处):编译器可以避免对行为不确定的构造发出警告一个可以覆盖的东西。
例如,请考虑以下代码:
class Base
{
public:
virtual void foo() { }
Base() { }
~Base();
};
void destroy(Base* b)
{
delete b;
}
当观察到b
时,许多编译器会向delete b
的非虚拟析构函数发出警告。如果一个类Derived
继承自Base
并且拥有自己的~Derived
析构函数,那么在动态分配的destroy
实例上使用Derived
通常会(根据规范行为是undefined)调用~Base
,但不会调用~Derived
。因此~Derived
的清理操作不会发生,这可能是坏的(尽管在大多数情况下可能不是灾难性的)。
如果编译器知道无法继承Base
,那么~Base
非虚拟就没问题,因为不会意外跳过派生清理。将final
添加到class Base
会为编译器提供不发出警告的信息。
我知道以这种方式使用final
会抑制Clang的警告。我不知道其他编译器是否在这里发出警告,或者他们是否在确定是否发出警告时考虑了最终结果。