Asked  10 Months ago    Answers:  5   Viewed   10 times

I have a pure abstract base and two derived classes:

struct B { virtual void foo() = 0; };
struct D1 : B { void foo() override { cout << "D1::foo()" << endl; } };
struct D2 : B { void foo() override { cout << "D1::foo()" << endl; } };

Does calling foo in Point A cost the same as a call to a non-virtual member function? Or is it more expensive than if D1 and D2 wouldn't have derived from B?

int main() {
 D1 d1; D2 d2; 
 std::vector<B*> v = { &d1, &d2 };

 d1.foo(); d2.foo(); // Point A (polymorphism not necessary)
 for(auto&& i : v) i->foo(); // Polymorphism necessary.

 return 0;
}

Answer: the answer of Andy Prowl is kind of the right answer, I just wanted to add the assembly output of gcc (tested in godbolt: gcc-4.7 -O2 -march=native -std=c++11). The cost of the direct function calls is:

mov rdi, rsp
call    D1::foo()
mov rdi, rbp
call    D2::foo()

And for the polymorphic calls:

mov rdi, QWORD PTR [rbx]
mov rax, QWORD PTR [rdi]
call    [QWORD PTR [rax]]
mov rdi, QWORD PTR [rbx+8]
mov rax, QWORD PTR [rdi]
call    [QWORD PTR [rax]]

However, if the objects don't derive from B and you just perform the direct call, gcc will inline the function calls:

mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:std::cout
call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)

This could enable further optimizations if D1 and D2 don't derive from B so I guess that no, they are not equivalent (at least for this version of gcc with these optimizations, -O3 produced a similar output without inlining). Is there something preventing the compiler from inlining in the case that D1 and D2 do derive from B?

"Fix": use delegates (aka reimplement virtual functions yourself):

struct DG { // Delegate
 std::function<void(void)> foo;
 template<class C> DG(C&& c) { foo = [&](void){c.foo();}; }
};

and then create a vector of delegates:

std::vector<DG> v = { d1, d2 };

this allows inlining if you access the methods in a non-polymorphic way. However, I guess accessing the vector will be slower (or at least as fast because std::function uses virtual functions for type erasure) than just using virtual functions (can't test with godbolt yet).

 Answers

3

Does calling foo in Point A cost the same as a call to a non-virtual member function?

Yes.

Or is it more expensive than if D1 and D2 wouldn't have derived from B?

No.

The compiler will resolve these function calls statically, because they are not performed through a pointer or through a reference. Since the type of the objects on which the function is called is known at compile-time, the compiler knows which implementation of foo() will have to be invoked.

Monday, August 16, 2021
2

I ran some timings on a 3ghz in-order PowerPC processor. On that architecture, a virtual function call costs 7 nanoseconds longer than a direct (non-virtual) function call.

So, not really worth worrying about the cost unless the function is something like a trivial Get()/Set() accessor, in which anything other than inline is kind of wasteful. A 7ns overhead on a function that inlines to 0.5ns is severe; a 7ns overhead on a function that takes 500ms to execute is meaningless.

The big cost of virtual functions isn't really the lookup of a function pointer in the vtable (that's usually just a single cycle), but that the indirect jump usually cannot be branch-predicted. This can cause a large pipeline bubble as the processor cannot fetch any instructions until the indirect jump (the call through the function pointer) has retired and a new instruction pointer computed. So, the cost of a virtual function call is much bigger than it might seem from looking at the assembly... but still only 7 nanoseconds.

Edit: Andrew, Not Sure, and others also raise the very good point that a virtual function call may cause an instruction cache miss: if you jump to a code address that is not in cache then the whole program comes to a dead halt while the instructions are fetched from main memory. This is always a significant stall: on Xenon, about 650 cycles (by my tests).

However this isn't a problem specific to virtual functions because even a direct function call will cause a miss if you jump to instructions that aren't in cache. What matters is whether the function has been run before recently (making it more likely to be in cache), and whether your architecture can predict static (not virtual) branches and fetch those instructions into cache ahead of time. My PPC does not, but maybe Intel's most recent hardware does.

My timings control for the influence of icache misses on execution (deliberately, since I was trying to examine the CPU pipeline in isolation), so they discount that cost.

Wednesday, June 9, 2021
 
HamidR
 
1

What is function hiding?

... is a form of name hiding. A simple example:

void foo(int);
namespace X
{
    void foo();
    
    void bar()
    {
        foo(42); // will not find `::foo`
        // because `X::foo` hides it
    }
}

This also applies to the name lookup in a base class:

class Base
{
public:
    void foo(int);
};

class Derived : public Base
{
public:
    void foo();
    void bar()
    {
        foo(42); // will not find `Base::foo`
        // because `Derived::foo` hides it
    }
};

What is function overriding?

This is linked to the concept of virtual functions. [class.virtual]/2

If a virtual member function vf is declared in a class Base and in a class Derived, derived directly or indirectly from Base, a member function vf with the same name, parameter-type-list, cv-qualification, and ref-qualifier (or absence of same) as Base::vf is declared, then Derived::vf is also virtual (whether or not it is so declared) and it overrides Base::vf.

class Base
{
private:
    virtual void vf(int) const &&;
    virtual void vf2(int);
    virtual Base* vf3(int);
};

class Derived : public Base
{
public: // accessibility doesn't matter!
    void vf(int) const &&; // overrides `Base::vf(int) const &&`
    void vf2(/*int*/);     // does NOT override `Base::vf2`
    Derived* vf3(int);     // DOES override `Base::vf3` (covariant return type)
};

The final overrider becomes relevant when calling a virtual function: [class.virtual]/2

A virtual member function C::vf of a class object S is a final overrider unless the most derived class of which S is a base class subobject (if any) declares or inherits another member function that overrides vf.

I.e. if you have an object of type S, the final overrider is the first overrider you see when traversing the class hierarchy of S back to its base classes. The important point is that the dynamic type of the function-call expression is used in order to determine the final overrider:

Base* p = new Derived;
p -> vf(42);    // dynamic type of `*p` is `Derived`

Base& b = *p;
b  . vf(42);    // dynamic type of `b` is `Derived`

What is the difference between overriding and hiding?

Essentially, the functions in the base class are always hidden by functions of the same name in a derived class; no matter if the function in the derived class overrides a base class' virtual function or not:

class Base
{
private:
    virtual void vf(int);
    virtual void vf2(int);
};

class Derived : public Base
{
public:
    void vf();     // doesn't override, but hides `Base::vf(int)`
    void vf2(int); // overrides and hides `Base::vf2(int)`
};

To find a function name, the static type of an expression is used:

Derived d;
d.vf(42);   // `vf` is found as `Derived::vf()`, this call is ill-formed
            // (too many arguments)

How do they relate to function overloads?

As "function hiding" is a form of name hiding, all overloads are affected if the name of a function is hidden:

class Base
{
private:
    virtual void vf(int);
    virtual void vf(double);
};

class Derived : public Base
{
public:
    void vf();     // hides `Base::vf(int)` and `Base::vf(double)`
};

For function overriding, only the function in the base class with the same arguments will be overriden; you can of course overload a virtual function:

class Base
{
private:
    virtual void vf(int);
    virtual void vf(double);
    void vf(char);  // will be hidden by overrides in a derived class
};

class Derived : public Base
{
public:
    void vf(int);    // overrides `Base::vf(int)`
    void vf(double); // overrides `Base::vf(double)`
};
Saturday, June 12, 2021
 
1

In order to make use of the dynamic polymorphism provided by virtual functions (distinguishing between derived and base classes during runtime), you need to access the derived class object via the base class pointer or reference.

I've commented out your code where confusion might have taken place:

int main( int argc , char ** argv )
{

    Animal a("A");
    a.eat();

    Dog b("b");
    b.eat();

    // Make a reference (alias) to Animal object and set it to the object a. 
    // From this point on, whenever you write c, think "a".
    Animal & c = a;
    // So, this is a.eat()
    c.eat();

    // This is a = b (Animal = Dog): DANGER! SLICING! Here, the assignment operator
    // slices the derived object and only assigns the base object "part" (remember, 
    // read "a", where you see "c" in your code): 
    // a.operator=(const A& b)
    c = b;
    // a.eat() = a is object of type A, so naturally, here you call A::eat()
    c.eat();

    return 0;
}
Tuesday, August 31, 2021
 
jcb
 
jcb
3

One benefit is in implementing the template method pattern:

class Base {

 public :
  void doSomething() {
    doSomething1();
    doSomething2();
    doSomething3();
  }
 private:
   virtual void doSomething1()=0;
   virtual void doSomething2()=0;
   virtual void doSomething3()=0;
};


class Derived : public Base {
  private:
   virtual void doSomething1() { ... }
   virtual void doSomething2() { .... }
   virtual void doSomething3() { .... }
}

This allows the derived classes to implement each piece of a certain logic, while the base class determines how to put these pieces together. And since the pieces don't make sense by themselves, they are declared private and so hidden from client code.

Wednesday, September 29, 2021
 
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :
 
Share