Virtual function

Pages: 1234
Hello. I have a little problem in order to understand why some functions should be virtual. I read often that a C++ virtual function is a member function in the base class that we redefine in a derived class. Ok I understand the rule, but where is the advantage? I found some examples on the web, but it is always too basic - something to get an output or a single value - nothing really clever. Do you know an example which shows clearly the plus of the virtual functions? Thank you for your help ++
I'll razzle dazzle you with something "unrelated".

Lets say I have a fancy class X that has data to print. I'd like to write:
1
2
X a
std::cout << a;

What does the second line require? An overloaded operator<<
1
2
3
4
5
std::ostream& operator<< ( std::ostream& lhs, const X& rhs )
{
  // send members of rhs into stream lhs here
  return lhs;
}

What if I want to do:
1
2
3
X a
std::ofstream out( "sample.txt" );
out << a;

What extra do I have to do? Nothing.

But, but ... the std::ofstream is a different type than std::ostream?
Sure, one inherits the other, but our operator<< "uses" the base class.

We only call the members of base class. Yet, the ofstream behaves differently -- writes to file, rather than to screen.
Like, the behaviour of the object depends on its actual type, even though we seem to call the same functions?

Our operator<< for X is generic; it works with all (std) output streams.
We do not need to care about how file-stream and stringstream do the actual work.

Somewhere there the virtual functions make the difference, don't they?
How would you implement standard library streams (without virtual functions)?
Last edited on
Let's say you have a base class Shape.
From that you derive several other classes: Triangle, Square, Rhombus.
All the derived classes have a perimeter.
If you have a pointer to a Shape, you want to get its perimeter regardless of which derived class it is.
1
2
3
4
5
class Shape
{  
public: 
    virtual double get_perimeter() = 0;  // pure virtual
};

Note that we made get_perimeter() a pure virtual since the perimeter of the base class object makes no sense.

Now we can define our derived classes.
1
2
3
4
5
class Square : public Shape
{
public: 
    double get_perimeter() override;
};

So when you call shape->get_perimeter() you will invoke the function for the derived shape.

edit: class Square was missing : public Shape
Last edited on
In a car game I did some time ago I had a Driver class with a virtual drive function.

1
2
3
4
5
6
class Driver
{
  ...
    virtual int drive(int carId, const Track& track) const = 0;
  ...
};

The track contained the state of the race track (including all the cars) and the carId told the driver which car it was controlling.

The int return value is actually a bit mask deciding what the car should do on the current frame. If the first bit is set it means the car should accelerate, the second bit means break, the third bit means turn left and the fourth bit means turn right.

For the computer controlled players there was an AIDriver class that inherited from Driver. Its drive function used the track and carId to figure out what it should do.

I also had a KeyboardDriver, a JoystickDriver and a MouseDriver class which were used for the human controlled players depending on what input device they were using. They didn't actually make use of the track and carId parameters because their decision was purely based on user input.

So before the race started each player were given a suitable Driver object. When the race had started the game logic didn't need to care about there being different kind of drivers. It just called the drive functions each frame to find out how the cars were driving.

This would have made it very easy for me to add another input device if I wanted. For example, if I wanted to allow the players to control their cars using a touch device I could have added a TouchDriver class to handle that. If I wanted to add different "AI Driver" classes with varying driving behaviour I could have added one class for each behaviour (e.g. DrunkAIDriver, CarefulAIDriver, etc.). I would also need to decide when these should be used and update the code that sets up the drivers but the actual game logic code would not need to change.
Last edited on
Thank you very much for all your clever explanations my friends. I really appreciate that you took time so as to correct my point of view. I understand how it works. It's clear.
@Peter87 your example is really solid, and I understand the way you created some hierarchy in your project. It stays a little bit confusing for me...
@AbstractionAnon ok this is more easy for me to understand why a virtual function is better than another concept. However (with shape perimeter) I can simplify reading of this code separating functions according to their shape. There is an example here :
https://www.includehelp.com/cpp-programs/find-the-perimeter-of-shapes-using-class.aspx
Yes, you can always call the derived classes' perimeter() function, BUT that assumes you know what kind of shape it is. The virtual approach lets you deal with the shape as an abstract object. You don't care what the shape is as long as it has a perimeter() function. Granted the shape example is contrived, but it demonstrates the power of virtual.

I have a game which has both human and computer players. I have a Player base class which has all the common logic for both human and computer players. The are (surprise) Computer and Human classes derived from the Player class. These classes contain the logic unique to the type of player. The logic in the program can call a player function without caring which type of player it is dealing with.

It follows our Shape example:
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
class Player
{
public:
  string get_player_name(int player_num) = 0;  // Pure virtual
};

class Human : public Player
{
public:
    string get_player_name(int player_num)
    {  string temp;
       cout << "Enter player " << player_num << " name: ";
       cin >> temp;  
       return temp;
    }
};

class Computer : public Player
{
public: 
    string get_player_name(int player_num)
    {    stringstream ss;
         ss << "Computer player " << player_num;
         return ss.str();
    }
};

int main()
{
  constexpr int NUM_PLAYERS = 4;
  string player_names[NUM_PLAYERS];
  Players players[NUM_PLAYERS];
  //  Allocate the Human and Computer objects
  //  ...
  //  Get the name of each player
  for (int i=0; i<NUM_PLAYERS; i++)
    player_names[i] = players[i].get_player_name(i+1);  // Don't care if it's Human or Computer
}

In the above example, the for loop at line 36 shows an example where we don't care whether the player object is Human or Computer.
Last edited on
OK. This is more understable this way. Thank you @AbstractionAnon.
According to your explanation and your code, I reached something efficient with area computation ++

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <iostream>  
constexpr auto M_PI = 3.14159265358979323846;

class Shape {

protected:
    double x, y;

public:
    void set_shape_dim(double i = 0, double j = 0) 
    {
        x = i;
        y = j;

        this->compute_area();
    }

    virtual void compute_area()
    {
        std::cout << "No computation here" << std::endl;
    }
};

struct triangle : public Shape {

    void compute_area() override
    {
        std::cout << "Isocele Triangle with height ";
        std::cout << x << " and base " << y;
        std::cout << " has an area of ";
        std::cout << (x * 0.5 * y) << std::endl;
    }
};

struct rectangle : public Shape {

    void compute_area() override
    {
        x == y ? std::cout << "Square with dimension " : std::cout << "Rectangle with dimensions ";
        std::cout << x << "*" << y;
        std::cout << " has an area of ";
        std::cout << (x * y) << std::endl;
    }
};

struct circle : public Shape {

    void compute_area() override
    {
        std::cout << "Circle with radius " << x;
        std::cout << " has an area of " << (M_PI * x * x) << std::endl;
    }
};

int main()
{
    Shape *s = new Shape();

    triangle t;
    rectangle r;
    circle c;

    s = &t;
    s->set_shape_dim(8.0, 5.0);

    s = &r;
    s->set_shape_dim(7.0, 5.0);

    s = &c;
    s->set_shape_dim(9.0);

    return 0;
}
Last edited on
And an answer calls another question - as usual :)

° The override specifier seems useless - but the function overrides the base class function. What it means?

° Do I have to delete the dynamic memory allocation when I use a derived class?
When I use delete s, it crashes at end...

° Inside the base class I call directly the computation function? Is it safe?

Last edited on
° The override specifier seems useless - but the function overrides the base class function. What it means?

The override specifier helps you catch errors. If you misspell the function name or get the number of parameters or the type of the parameters wrong then it will give you a compilation error if you use override. If you don't use it then the code will compile but it will not override so you don't get the behaviour you want.

° Inside the base class I call directly the computation function? Is it safe?

Yes. The only thing you need to watch out for is if you call it from the constructor then it will always call the base class function.

° Do I have to delete the dynamic memory allocation when I use a derived class?

Yes, you always have to use delete to destroy objects created with new, otherwise you'll have a memory leak.

When I use delete s, it crashes at end...

It's because s no longer points to the dynamically allocated object. You never use that object so why create it in the first place?

Note that you probably want to declare the destructor as virtual in the base class, otherwise it won't work if you delete an object of a derived class through a base class pointer.

1
2
Shape* s = new Triangle();
delete s; // UB if Shape::~Shape() is not virtual 
Last edited on
You didn't show where you tried to delete s.
I suspect it was at line 71.

At line 71, s points to c which is on the stack.
You can't delete something on the stack.
You should be deleting s at line 62.
At line 63, you overwrite s with the address of t thereby losing the previous value of s and resulting in a memory leak.


Do I have to delete the dynamic memory allocation when I use a derived class?

The C++ ISO Committee chose to create dynamic memory management easier when they added smart pointers to the C++11 stdlib.
https://en.cppreference.com/w/cpp/memory

Yes, before C++11 there was auto_ptr, a wreak searching for a train.

C++11 added several smart pointers, and plugged the ownership issues that auto_ptr had.

Use a smart pointer and let the C++ implementation deal with the memory allocation and deallocation. So you don't have to.

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
#include <memory>

// .....

int main()
{
   // constructing a smart pointer using new
   std::shared_ptr<Shape> s  { new Shape() };

   // using a helper function to construct a smart pointer
   std::shared_ptr<Shape> s2 { std::make_shared<Shape>(Shape()) };

   triangle  t;
   // rectangle r;
   // circle    c;

   // s = &t;
   s = std::make_shared<Shape>(t);
   s->set_shape_dim(8.0, 5.0);

   // s = &r;
   s = std::make_shared<Shape>(rectangle());
   s->set_shape_dim(7.0, 5.0);

   // s = &c;
   s = std::make_shared<Shape>(circle());
   s->set_shape_dim(9.0);

   // return 0;
}

The smart pointer implementation handles the memory management now.
Last edited on
From a design perspective, virtual functions is how you implement object oriented programming using dynamic polymorphism in C++.

The virtual functions are the objects methods. These methods can be overridden by derived classes, and the correct method will be called when the object is accessed through a pointer/reference. RTTI was designed to assist, and so was dynamic_cast.

Remember, C++ is a multi-paradigm language. A class/struct is used for many different notions. To make sense of things, it helps to understand the ideas behind the features, or you'll just end up writing spaghetti code.
Spaghetti code. Nice picture for bad code... Thanks :)

I think that I finally understand the main principle under virtual functions, but I need to practice again and again. Please can you take a look at the code about the computation area which I improved in order to get an output declaring the object destruction for each derived class/structure? A last stupid question : is this a polymorphism code? Is this its name?

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <iostream>  
constexpr auto M_PI = 3.14159265358979323846;

class Shape {

protected:
    double x = 0;
    double y = 0;
    std::string n = "undefined";

public:
    virtual void set_name(std::string name){ n = name; }

    void set_shape_dim(double i = 0, double j = 0) 
    {
        x = i;
        y = j;

        this->compute_area();
    }

    virtual void compute_area()
    {
        std::cout << "No computation here" << std::endl;
    }

    virtual ~Shape()
    {
        std::cout << "Destructing base " << typeid(*this).name() << " and its derived " << n << std::endl;
    }
};

struct triangle : public Shape {

    void compute_area()override
    {
        std::cout << "Isocele Triangle with height ";
        std::cout << x << " and base " << y;
        std::cout << " has an area of ";
        std::cout << (x * 0.5 * y) << std::endl;
    }
};

struct rectangle : public Shape {

    void compute_area()override
    {
        x == y ? std::cout << "Square with dimension " : std::cout << "Rectangle with dimensions ";
        std::cout << x << "*" << y;
        std::cout << " has an area of ";
        std::cout << (x * y) << std::endl;
    }
};

struct circle : public Shape {

    void compute_area()override
    {
        std::cout << "Circle with radius " << x;
        std::cout << " has an area of " << (M_PI * x * x) << std::endl;
    }
};

int main()
{
    Shape *s = new Shape();

    triangle t;
    rectangle r;
    circle c;

    s = &t;
    s->set_name(typeid(t).name());
    s->set_shape_dim(8.0, 5.0);

    s = &r;
    s->set_name(typeid(r).name());
    s->set_shape_dim(7.0, 5.0);

    s = &c;
    s->set_name(typeid(c).name());
    s->set_shape_dim(9.0);

    return 0;
}



Isocele Triangle with height 8 and base 5 has an area of 20
Rectangle with dimensions 7*5 has an area of 35
Circle with radius 9 has an area of 254.469
Destructing base class Shape and its derived struct circle
Destructing base class Shape and its derived struct rectangle
Destructing base class Shape and its derived struct triangle
Last edited on
As I pointed out previously, you have a memory leak at line 72.

At line 66, you allocate s.
At line 72, you reassign s to the address of t.
This wipes out the previous value of s creating a memory leak.
Better?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main()
{
    triangle t;
    rectangle r;
    circle c;

    t.set_name(typeid(t).name());
    t.set_shape_dim(8.0, 5.0);

    r.set_name(typeid(r).name());
    r.set_shape_dim(7.0, 5.0);

    c.set_name(typeid(c).name());
    c.set_shape_dim(9.0);

    return 0;
}


Or using a smart pointer as said George P
Last edited on
If you do not need pointers and/or dynamic allocations then it's usually better to not use them because it complicates the code.

Note that the string returned from typeid(type).name() is implementation defined. It's fine in debug print statements like this but you probably don't want to use it in real code, at least not if you aim to get a consistent and human readable result.

Destructing base 5Shape and its derived 6circle
Destructing base 5Shape and its derived 9rectangle
Destructing base 5Shape and its derived 8triangle
Last edited on
1. Can you explain why you did add the new Shape() in:
1
2
3
    Shape *s = new Shape();
    triangle t;
    s = &t;



2. Some libraries do have M_PI:
main.cpp:2:16: error: expected unqualified-id
constexpr auto M_PI = 3.14159265358979323846;
               ^
/root/emsdk/upstream/emscripten/cache/sysroot/include/math.h:375:25: note: expanded from macro 'M_PI'
#define M_PI            3.14159265358979323846  /* pi */
                        ^
1 error generated.



3. Typeid-names seem not to be identical to identifiers in code:
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>

class Shape {
    std::string name = "undefined";
protected:
    double x = 0;
    double y = 0;
    Shape( const std::string& name )
    : name( name )
    {}

public:
    void set_shape_dim(double i = 0, double j = 0) 
    {
        x = i;
        y = j;

        this->compute_area();
    }

    virtual void compute_area()
    {
        std::cout << "No computation here" << std::endl;
    }

    virtual ~Shape()
    {
        std::cout << "Destructing base " << typeid(*this).name() << " and its derived " << name << std::endl;
    }
};

struct triangle : public Shape {
    triangle()
    : Shape( "triceratops" )
    {};

    void compute_area()override
    {
        std::cout << "Isocele Triangle with height ";
        std::cout << x << " and base " << y;
        std::cout << " has an area of ";
        std::cout << (x * 0.5 * y) << std::endl;
    }
    
    ~triangle()
    {
        std::cout << "Destructing " << typeid(*this).name() << std::endl;
    }    
};


int main()
{
    triangle t;
    Shape* s = &t;
    s->set_shape_dim(8.0, 5.0);
}

Isocele Triangle with height 8 and base 5 has an area of 20
Destructing 8triangle
Destructing base 5Shape and its derived triceratops


4. Some say "runtime polymorphism"
As someone said earlier, the example is a bit contrived.

I think a function should do what its name says.

Having a function named "compute_area" that prints a message describing the state of the shape and what the area is would not have been my first guess if I just read the name. Instead I would have guessed that it simply returned the area (in that case a better name might have been "get_area" or simply "area"), or perhaps that it calculated the area and stored it somewhere to be retrieved later, but in any case without printing anything. I think a better name would have been "print_area_message" but there are probably many other good names.

The same thing with the "set_shape_dim" function. The name suggests that it only modifies the dimensions of the shape but surprisingly it also prints things (because it calls "compute_area").

I don't think shapes is the best example of using virtual functions. Usually you need to know exactly what shape it is in order to do something interesting. There is for example no easy way to implement collision detection between two objects of different shapes using virtual functions. You could use the visitor pattern but that becomes complicated and requires lots of "boilerplate" code. It might be easier to solve it some other way such as using std::variant instead.
Last edited on
That said, I tried hard to think of a simple reasonably good example using shapes and this is what I came up with:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#include <iostream>
#include <string>
#include <cstdlib>
#include <ctime>
#include <memory>

class Shape
{
public:
	virtual std::string name() const = 0;
	virtual void draw() const = 0;
	virtual int area() const = 0;
	virtual ~Shape() = default;
};

struct Triangle : public Shape
{
public:
	Triangle(double base, double height)
	:	base(base),
		height(height)
	{
	}
	
	std::string name() const override
	{
		return "triangle";
	}
	
	void draw() const override
	{
		for (int y = 1; y <= height; ++y)
		{
			for (int x = 1; x <= 1 + (base - 1) * y / height; ++x)
			{
				std::cout << '*';
			}
			std::cout << '\n';
		}
	}
	
	int area() const override
	{
		return base * height / 2;
	}
	
private:
	int base;
	int height;
};

struct Circle : public Shape
{
public:
	Circle(double radius)
	:	radius(radius)
	{
	}
	
	std::string name() const override
	{
		return "circle";
	}
	
	void draw() const override
	{
		for (int y = -radius; y <= radius; ++y)
		{
			for (int x = -radius; x <= radius; ++x)
			{
				if (x * x + y * y < radius * radius)
				{
					std::cout << '*';
				}
				else
				{
					std::cout << ' ';
				}
			}
			std::cout << '\n';
		}
	}
	
	int area() const override
	{
		return 3 * radius * radius;
	}
	
private:
	double radius;
};

void tell_story(const Shape& house_shape)
{
	std::cout << "Once upon a time a young boy walked through the woods.\n";
	std::cout << "Then he saw something strange!\n";
	std::cout << "It was a house shaped like a " << house_shape.name() << "!\n";
	std::cout << "===================================\n";
	std::cout << " This is what the house looked like:\n";
	house_shape.draw();
	std::cout << "===================================\n";
	std::cout << "The boy went up to the house and measured its front wall.\n";
	std::cout << "It was " << house_shape.area() << " square metres.\n";
	std::cout << "The boy tried to enter the house but ";
	if (house_shape.area() < 10)
	{
		std::cout << "he couldn't fit.\n";
	}
	else if (house_shape.area() < 50)
	{
		std::cout << "got scared by a goblin.\n";
	}
	else
	{
		std::cout << "the door handle was too high up for him to reach it.\n";
	}
	std::cout << "The boy ran home to his mum and lived happily ever after.\n";
	std::cout << "The end.\n";
}

It's still not a very realistic example, and I used only integers for simplicity, but the point is that you can write a function like that "tell_story" function that can accept any Shape object.

You can call tell_story with any house shape you want.
1
2
3
4
5
6
7
Circle house_shape1(5);
tell_story(house_shape1);

std::cout << "\n\n\n";

Triangle house_shape2(15, 6);
tell_story(house_shape2);

Or you could generate a random shape and use that to tell the story.
1
2
3
4
5
6
7
8
9
10
11
12
13
std::srand(std::time(nullptr));

std::unique_ptr<Shape> house_shape;
if ((rand() % 2) == 0)
{
	house_shape = std::make_unique<Circle>(1 + rand() % 8);
}
else
{
	house_shape = std::make_unique<Triangle>(2 + rand() % 14, 2 + rand() % 14);
}

tell_story(*house_shape);

This works nicely because the "tell_story" code does not care about what kind of shape it is. It just uses the interface defined by the Shape base class.
Last edited on
I cannot express all my gratitude to you. I really appreciate your kindness in educating me about virtual functions. The different examples help me to understand more. Thank you!!! First of all, I don't understand why you got this weird output. When I compile, I have a correct output according to the derived structure name :


Isocele Triangle with height 8 and base 5 has an area of 20
Rectangle with dimensions 7*5 has an area of 35
Circle with radius 9 has an area of 254.469
Destructing base class Shape and its derived struct circle
Destructing base class Shape and its derived struct rectangle
Destructing base class Shape and its derived struct triangle


Using the constructor in order to set the name is a good idea. Finally I reformulate my previous code. I know that there is no logical functions names - but this is just an exercise. @Peter87 your little game is an interesting model which could help beginners to understand how to use virtual functions...

Is this better this way? I read all your previous explanations. I know that sometimes I miss something and maybe it seems to you that I have no consideration according to a previous point. In fact, English is a foreign language for me - so I have a double wall beyond me :)

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <iostream>  
constexpr auto M_PI = 3.14159265358979323846;

class Shape {

protected:
    double x = 0;
    double y = 0;
    std::string n = "undefined";

    Shape(const std::string& name): n(name){}

public:
    void set_shape_dim(double i = 0, double j = 0) 
    {
        x = i;
        y = j;

        this->compute_area();
    }

    virtual void compute_area()
    {
        std::cout << "No computation here" << std::endl;
    }

    virtual ~Shape()
    {
        std::cout << "Destructing base " << typeid(*this).name() << " and its derived " << n << std::endl;
    }
};

struct triangle : public Shape { triangle() : Shape("_triangle"){};

    void compute_area()override
    {
        std::cout << "Isocele Triangle with height ";
        std::cout << x << " and base " << y;
        std::cout << " has an area of ";
        std::cout << (x * 0.5 * y) << std::endl;
    }
};

struct rectangle : public Shape { rectangle() : Shape("_rectangle") {};

    void compute_area()override
    {
        x == y ? std::cout << "Square with dimension " : std::cout << "Rectangle with dimensions ";
        std::cout << x << "*" << y;
        std::cout << " has an area of ";
        std::cout << (x * y) << std::endl;
    }
};

struct circle : public Shape { circle() : Shape("_circle") {};

    void compute_area()override
    {
        std::cout << "Circle with radius " << x;
        std::cout << " has an area of " << (M_PI * x * x) << std::endl;
    }
};

int main()
{
    triangle t;
    rectangle r;
    circle c;

    t.set_shape_dim(8.0, 5.0);
    r.set_shape_dim(7.0, 5.0);
    c.set_shape_dim(9.0);

    return 0;
}



Isocele Triangle with height 8 and base 5 has an area of 20
Rectangle with dimensions 7*5 has an area of 35
Circle with radius 9 has an area of 254.469
Destructing base class Shape and its derived _circle
Destructing base class Shape and its derived _rectangle
Destructing base class Shape and its derived _triangle
Last edited on
Pages: 1234