Dowemo
0 0 0 0


Question:

C++11 will allow to mark classes and virtual method to be final to prohibit deriving from them or overriding them.

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_;
};

This is very useful, because it tells the reader of the interface something about the intent of the use of this class/method. That the user gets diagnostics if he tries to override might be useful, too.

But is there an advantage from the compilers point of view? Can the compiler do anything different when he knows "this class will never be derived from" or "this virtual function will never be overridden"?

For final I mainly found only N2751 referring to it. Sifting through some of the discussions I found arguments coming from the C++/CLI side, but no clear hint why final may be useful for the compiler. I am thinking about this, because I also see some disadvantages of marking a class final: To unit-test protected member functions one can derive a class and insert test-code. Sometimes these classes are good candidates to be marked with final. This technique would be impossible in these cases.


Best Answer:


Virtual calls to functions are slightly more costly that normal calls. In addition to actually performing the call, the runtime must first determine which function to call, which oftens leads to:

  • Locating the v-table pointer, and through it reaching the v-table
  • Locating the function pointer within the v-table, and through it performing the call
  • Compared to a direct call where the address of the function is known in advance (and hard-coded with a symbol), this leads to a small overhead. Good compilers manage to make it only 10%-15% slower than a regular call, which is usually insignificant if the function has any meat.

    A compiler's optimizer still seeks to avoid all kinds of overhead, and devirtualizing function calls is generally a low-hanging fruit. For example, see in C++03:

    struct Base { virtual ~Base(); };
    struct Derived: Base { virtual ~Derived(); };
    void foo() {
      Derived d; (void)d;
    }

    Clang gets:

    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
    }

    As you can see, the compiler was already smart enough to determine that d being a Derived then it is unnecessary to incur the overhead of virtual call.

    In fact, it would optimize the following function just as nicely:

    void bar() {
      Base* b = new Derived();
      delete b;
    }

    However there are some situations where the compiler cannot reach this conclusion:

    Derived* newDerived();
    void deleteDerived(Derived* d) { delete d; }

    Here we could expect (naively) that a call to deleteDerived(newDerived()); would result in the same code than before. However it is not the case:

    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
    }

    Convention could dictate that newDerived returns a Derived, but the compiler cannot make such an assumption: and what if it returned something further derived ? And thus you get to see all the ugly machinery involved in retrieving the v-table pointer, selecting the appropriate entry in the table and finally performing the call.

    If however we put a final in, then we give the compiler a guarantee that it cannot be anything else:

    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
    }

    In short: final allows the compiler to avoid the overhead of virtual calls for the concerned functions in situations where detecting it is impossible.




    Copyright © 2011 Dowemo All rights reserved.    Creative Commons   AboutUs