C++ 编译器如何从C++';什么是新的最终关键词?

C++ 编译器如何从C++';什么是新的最终关键词?,c++,compiler-construction,final,c++11,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 { in

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
成员函数的调用,指向最派生的类型

如果今天有编译器这样做的话,我会非常惊讶,但这似乎是未来十年左右可能实现的事情


如果能够推断(在没有朋友的情况下)在
final
类中标记为
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
    派生的
    ,因此不需要产生虚拟调用的开销

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

    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
    }
    

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

    取决于您如何看待它,编译器还有一个进一步的好处(尽管这一好处只会对用户有所帮助,所以可以说这不是编译器的好处):编译器可以避免对行为不确定的构造发出警告,因为这些构造是可重写的

    例如,考虑这个代码:

    class Base
    {
      public:
        virtual void foo() { }
        Base() { }
        ~Base();
    };
    
    void destroy(Base* b)
    {
      delete b;
    }
    
    当观察到
    delete b
    时,许多编译器将发出
    b
    的非虚拟析构函数警告。如果类
    派生的
    继承自
    并拥有自己的
    ~派生的
    析构函数,则在动态分配的
    派生的
    实例上使用
    销毁
    通常会(根据规范行为未定义)调用
    ~基
    ,但它不会调用
    ~派生的
    。因此,
    ~派生的
    的清理操作不会发生,这可能是不好的(尽管在大多数情况下可能不是灾难性的)

    但是,如果编译器知道不能从继承
    Base
    ,那么
    ~Base
    是非虚拟的就没有问题,因为不能意外跳过派生清理。将
    final
    添加到
    class Base
    中,可以向编译器提供不发出警告的信息


    我知道这样使用
    final
    会抑制带有叮当声的警告。我不知道其他编译器是否在此处发出警告,或者在确定是否发出警告时是否考虑了最终性。

    +1,为什么这是一条注释。这是一个值得回答的问题。一开始是一个评论,因为我认为这将是一个一次性的“这里可能有答案”提示/评论,直到我意识到我已经编辑了足够多的评论,基本上写了一个答案,而不是一次性的提示!在最好的情况下,这会允许内联那些不再是虚拟的函数调用吗?那个
    class Base
    {
      public:
        virtual void foo() { }
        Base() { }
        ~Base();
    };
    
    void destroy(Base* b)
    {
      delete b;
    }