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

时间:2011-09-24 11:50:42

标签: c++ compiler-construction final c++11

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标记的好人选。在这些情况下,这种技术是不可能的。

3 个答案:

答案 0 :(得分:36)

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

对派生的virtual类型进行final调用分派,您可以确定没有其他内容来自该类型。这意味着(至少在理论上)final关键字可以在编译时正确地解析一些virtual调用,这将使得{{1}上无法实现的一些优化成为可能。调用。

例如,如果您有virtual,其中delete most_derived_ptr是指向派生的most_derived_ptr类型的指针,那么编译器可以简化对final的调用析构函数。

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

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

virtual friend中,标记为protected的内容也可能有效地变为{{}} {1}}。

答案 1 :(得分:30)

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

  1. 找到v-table指针,并通过它到达v-table
  2. 在v表中找到函数指针,并通过它执行调用
  3. 与预先知道函数地址(并用符号硬编码)的直接调用相比,这会导致较小的开销。好的编译器设法使它比常规调用慢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
    }
    

    正如您所看到的,编译器已足够聪明,可以确定dDerived,因此无需承担虚拟呼叫的开销。

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

    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的警告。我不知道其他编译器是否在这里发出警告,或者他们是否在确定是否发出警告时考虑了最终结果。