Inheritance and virtual functions

Greetings!

I try to understand the role of the virtual keyword when changing the signature of a member function in a derived class compared to a base class (I am aware that this is bad practice, but I still want to understand).

Consider the following example from a book. There are two lines that the book says lead to an error. I added my understanding and questions in the comments.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class base_class {
public:
    virtual void vf1();
    virtual void vf2();
    virtual void vf3();
};

class derived_class : public base_class {
public:
    virtual void vf1();

    virtual void vf2(int);  // the change in parameter list seems to be allowed.

    virtual char vf3();     // ERROR. why is this change in signature not allowed when changing the parameter list is allowed?
};

int main () {
    derived_class d;
    base_class *bp = &d;  

    bp->vf1(); // derived_class::vf1() since virtual

    bp->vf2(); // base_class::vf2() as derived_class does not have function with this signature

    d.vf2(); // not possible for same reason
             // due to re-using the name vf2 in derived_class for a function 
             // with another signature than in base_class, the function base_class::vf2() was not inherited.
             
    d.vf2(7);  // works

    bp->vf2(7); // ERROR because base_class does not have corresponding
                // function. But I thought the role of the "virtual" keyword
                // is to ensure that the one function is executed that belongs
                // to the object that the pointer points to. So it should be enough
                // if vf2(int) exists in the derived_class...
}
Last edited on
Someone else can probably better explain how overloads don't cross class boundaries unless you specifically allow each one.

But I just want to say: This is one reason to use the override specifier when possible. (If your book was pre-C++11, it might not have been available.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class base_class {
public:
    virtual void vf1();
    virtual void vf2();
};

class derived_class : public base_class {
public:
    virtual void vf1();

    //virtual void vf2(int); // uncomment --> still compiles
    virtual void vf2(int) override; // uncomment --> fails to compile, because you aren't actually overriding anything!

};

int main() { }

error: 'vf2' marked 'override' but does not override any member functions
Last edited on
> the change in parameter list seems to be allowed.
> the function base_class::vf2() was not inherited.

It is inherited, but the base class function is hidden ie. it is not found by unqualified lookup.

A function with the same name but different parameter list does not override the base function of the same name, but hides it: when unqualified name lookup examines the scope of the derived class, the lookup finds the declaration and does not examine the base class.
https://en.cppreference.com/w/cpp/language/virtual#In_detail


> why is this change in signature not allowed?

Arbitrary changes in the return type is not allowed; the type of the expression Derived::f() must be known at compile time, even if the call is dispatched at run time.
Essentially for the same reason that ( x==y ? 1234 : "abcd" ) is not allowed.

If the function Derived::f overrides a function Base::f, their return types must either be the same or be covariant.
...
When a virtual function call is made, the type returned by the final overrider is implicitly converted to the return type of the overridden function
https://en.cppreference.com/w/cpp/language/virtual#Covariant_return_types
Thanks guys,

I am still not clear on this:
1
2
3
4
5
    bp->vf2(7); // ERROR because base_class does not have corresponding
                // function. But I thought the role of the "virtual" keyword
                // is to ensure that the one function is executed that belongs
                // to the object that the pointer points to. So it should be enough
                // if vf2(int) exists in the derived_class... 

After having
1
2
3
4
5
class derived_class : public base_class {
public:

    virtual void vf2(int);  // the change in parameter list seems to be allowed.
}

I would assume that the compiler knows that it simply needs to look up the list of virtual functions associated to derived_class during run time.
Why would it be necessary that the function also exists in the parent class?


Another question:
Is it true that virtual function calls lead to slowed runtime?
I.e. if efficiency is an issue and I wanted to call derived_class::vf1(), instead of doing
1
2
3
4
5
6
 
int main () {
    derived_class d;
    base_class *bp = &d;  

    bp->vf1(); // virtual function call 

I should rather do
1
2
3
4
5
6
     // continue main

     d.vf1();      // call by name

     derived_class *dp = &d;
     dp->vf1();    // call via pointer of type derived_class* 
.
bp is a base-class pointer: it can only be used to point to member functions that are declared in the base class (even if their definition has to wait until the derived class).

There is no vf2(int) function in the base class. The only similarly-named function there takes no arguments.

Anything which has to be decided at runtime will, in principle, be a tiny bit slower: but I doubt that you would be able to detect the difference, and it would be far outweighed by the benefits of polymorphism.
Last edited on
Is it true that virtual function calls lead to slowed runtime?

If class has a virtual function, then it does have a lookup table.
When you call virtual function (be it via bp or dp), the pointer to implementation is looked up from that table (during runtime).

If you want the call to be tiny bit faster, then the function cannot be virtual.
> Is it true that virtual function calls lead to slowed runtime?

The optimiser may not be able to inline the virtual call.
For a (typically small) function which is called a large number of times, this (the inability to optimise across the function call) may make for a significant difference in performance.

For example:

a.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct A
{
    virtual int foo() = 0 ;
    int bar() { return 0 ; }
};

struct B : A
{
    virtual int foo() override { return ++x ; }
    int bar() { return ++x ; }

    int x = 0 ;
};

B b ;
A& a = b ;


main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <ctime>

struct A
{
    virtual int foo() = 0 ;
    int bar() { return 0 ; }
};

struct B : A
{
    virtual int foo() override { return ++x ; }
    int bar() { return ++x ; }

    int x = 0 ;
};

int main()
{
    extern A& a ;
    extern B b ;
    const int N = 1'000'000'000 ;

    {
        b.x = 0 ;
        const auto start = std::clock() ;
        for( int i = 0 ; i < N ; ++i ) a.foo() ;
        const auto end = std::clock() ;
        std::cout << "dynamic dispatch: " << std::fixed << (end-start) * 1.0 / CLOCKS_PER_SEC << " seconds\n" ;
        if( a.foo() == (N+1) ) std::cout << "ok\n\n" ;
    }

    {
        b.x = 0 ;
        const auto start = std::clock() ;
        for( int i = 0 ; i < N ; ++i ) b.foo() ;
        const auto end = std::clock() ;
        std::cout << " static dispatch: " << std::fixed << (end-start) * 1.0 / CLOCKS_PER_SEC << " seconds\n" ;
        if( b.foo() == (N+1) ) std::cout << "ok\n\n" ;
    }
} 

ln -s /Archive2/dd/44d0138bf6c3dc/main.cpp a.cpp

g++ -std=c++20 -O3 -Wall -Wextra -pedantic-errors -c a.cpp
g++ -std=c++20 -O3 -Wall -Wextra -pedantic-errors main.cpp a.o && ./a.out
echo =====================================
g++ -std=c++20 -O3 -Wall -Wextra -pedantic-errors -c a.cpp
clang++ -std=c++2a -O3 -Wall -Wextra -pedantic-errors main.cpp a.o && ./a.out
echo =====================================
dynamic dispatch: 0.396826 seconds
ok

 static dispatch: 0.000002 seconds
ok

=====================================
dynamic dispatch: 2.367067 seconds
ok

 static dispatch: 0.000001 seconds
ok

=====================================


http://coliru.stacked-crooked.com/a/d3518323010c6e19
Last edited on
The Hidden Performance Price of C++ Virtual Functions - Ivica Bogosavljevic - CppCon 2022
https://www.youtube.com/watch?v=kRdbqjw2WIs
Your example only has a single derived object, so it's difficult to find a reason not to do a direct call using that interface, rather than using dynamic polymorphism and call via the base class.

Real world code that uses dynamic polymorphism isn't like this. Often the actual concrete classes that are used, by library code, isn't known at the time of writing. There is no concrete derived class that you can use that you can call directly.

For example, ostream and istream have been base clases in the C++ standard library from the start. Derived classes based on these have appeared in many popular libraries; from Tools.h++ to Boost and beyond.

You're not comparing like with like, that's why you appear to have the luxury of a choice. In the real world, you don't.
Last edited on
Thank you guys!
Topic archived. No new replies allowed.