Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++: Do virtual function calls with a pointer to the derived class still have a vlookup

Just wondering, if I have a pointer to the most derived class, and call a virtual function on it, which the most derived class defines, does this still cause a lookup in the virtual table?

After all, at compile time, the compiler knows this class is the most derived, it knows it defines the virtual function, there is no ambiguity, so it should just treat it as a non virtual function?

Or am I missing something?

The reason I am asking is that I am writing a template which I want to derive from later to merge code, and the functions that differ will be implemented in the derived classes.

There is no need to define those functions as virtual in the template, but if that virtual call would get ignored later I am considering doing it anyway, purely to visualize to the implementer later which functions need to be still written.

like image 269
Cookie Avatar asked May 07 '14 12:05

Cookie


1 Answers

Disclaimer: Compiler Optimization

This answer is about compiler optimization techniques. Your compiler may or may not support these. Even if it supports the technique you are trying to exploit, they may not be available at your chosen optimization level.

Your mileage may vary.

Most Derived Class

The compiler can indeed devirtualize the call if it knows that this is indeed the most derived class. How this could be achieved is discussed here. An example:

struct Base { virtual void call_me_virtual() = nullptr; };
struct Derived final : Base { void call_me_virtual() override { } };

void dosomething(Derived* d) {
    d->call_me_virtual();
}

Interestingly, if this is not done, you could always have someone else derive from your class in another translation unit, so the compiler would not be aware of that "more derived" class in your current translation unit.

Another way to ensure that the class must be most derived is to put it into an anonymous namespace:

struct Base { virtual void call_me_virtual() = nullptr; };

namespace {
    struct Derived : Base { void call_me_virtual() override { } };

    void dosomething(Derived* d) {
        d->call_me_virtual();
    }
}

This works, because Derived is not known outside the current translation unit, which implies that any more derived class must be inside the current translation unit. However, this means that dosomething cannot be (correctly) called from outside the current compilation unit, which is why I have given it internal linkage as well.

One exemption from this rule are compilers that are able to prove that the object originates from within their view, e.g. if Derived is the most derived type in your current translation unit and the object must always come from within the current translation unit, then it cannot be from any more derived type. The scope of this analysis may be widened by utilizing whole program optimization.

Known Type

The more common case is that of the compiler knowing exactly what type the object is, e.g. in both of these cases:

Derived d;
d.call_me_virtual();

Base* b = new Derived;
b->call_me_virtual();

The compiler may be able to deduce that d.call_me_virtual and b->call_me_virtual will both always resolve to Derived::call_me_virtual and thus devirtualize (and even inline) the call in that instance.

In the first case, this requires knowledge that an object of type Derived unlike an object of the very similar type Derived& can only be a Derived object and not something derived from that.

The second case requires static type analysis, which is being done by modern optimizing compilers. By seeing that b is always initialized with a pointer to a Derived object, the compiler is able to prove which function will be called by b->call_me_virtual.

Finalized Member function

A similar case happens when you finalize a single member function:

struct Base { virtual void call_me_virtual() = nullptr; };
struct Derived : Base { void call_me_virtual() override final { } };

void dosomething(Derived* d) {
    d->call_me_virtual();
}

While d may point to something derived from Derived, the call_me_virtual method may not be changed anymore, so the compiler knows that Derived::call_me_virtual will always be called, and can therefore devirtualize this call.

like image 129
gha.st Avatar answered Oct 01 '22 09:10

gha.st