Post

[C++] Chap 12 - Polymorphism and Virtual Functions

[C++] Chap 12 - Polymorphism and Virtual Functions

Virtual Function Basics

Polymorphism

  • Associating many meanings to one function
  • Virtual Function : used before it’s defined
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
class Figure {
public :
	(virtual) void draw() {
		cout<< "Figure::draw()\n";
		}
	void center() {
		cout<<"Erase\n";
		cout<<"Move to center\n";
		draw();
		Figure::draw(); //Explicitly call Figure::draw()
	}
};
class Circle : public Figure
{
	void draw(){
		cout<<"Circle::draw()\n";
	}
};
int main(void)
{
	Figure f;
	Circle c;
	f.center(); //Figure::draw() twice
	c.center(); //Circle::draw() then Figure::draw() if virtual
}
  • center() functions is applied to all inherited class by not redefining it
  • When writing center() the class Triangle wasn’t even written
  • Defining draw() function as virtual, the c.center() will call Circle::draw() function instead of Figure::draw()
  • Even though we don’t know Circle when defining center() , it calls Circle::draw()
  • The draw function of inherited function also becomes virtual (but can be omitted)
  • Polymorphism happens even though center() is not virtual function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Sale
{
public:
	virtual double bill() const;
};
bool operator < (const Sales& first, const Sale& second) {
	return (first.bill() < second.bill());
	// since it's virtual, fist.bill() calls first's class's bill
}
//compares two sales
int main()
{
	Sales simple(10.0);
	DiscountSale discount(11.0, 10);
	...
	if (discount<simple) {
	}
  • < computes discount.bill() <simple.bill() , each call of bill uses its own classes’ bill function due to virtual keyword
  • Due to reserved word “virtual” in declaration of member function bill, other member functions of Sale will use the version base on the object of derived class
  • Later, derived classes of Sale can define their own version of function ‘bill’ and it will automatically be called when invoking < operator
  • virtual qualifier always go in declaration not definition
  • In function declaration of derived class, it is not necessary to put virtual qualifier but it still is virtual funciton (Circle::draw())

Late binding

  • Virtual functions implement late binding
  • Tells compiler to wait until the function is used in program
  • Decide which definition to use based on the calling object
  • Disadvantage : Overhead (System resources) and Speed (late binding is on the fly)

Overriding

  • Virtual function definition changed in derived class : overridden
  • Redefined : Non-virtual functions changed
  • Overridden : Virtual functions changed

Pointers and Virtual Functions

Pure Virtual Functions

  • Make it a pure virtual function : Definition of base class does not exist
1
virtual void draw() = 0;

Abstract Base Classes

  • Pure virtual functions require no definition
    • Forces all derived classes to define their own version
  • Class with one or more pure virtual functions is called the “abstract base class
    • Can only be used as base class
    • No objects can ever be created from it (since it doesn’t have complete “definitions” of all its members
  • If derived class fails to define all pure virtual functions : the derived class itself becomes an abstract base class, too

Extended Type Compatibility

  • can assign derived object into Base type
  • but reverse is not true.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Pet
{
public:
	string name;
	virtual void print() const;
};
class Dog : public Pet
{
public:
	string breed;
	virtual void print() const;
};

int main(){
	Dog vdog;
	Pet vpet = vdog; // Upcasting
	cout<<vpet.breed; // Causes Slicing problem
	}

Slicing Problem fix

1
2
3
4
5
6
Pet *ppet;
Dog *pdog;
pdog = new Dog;
pdog->name = "Tiny";
pdog->breed = "Great Dane";
ppet = pdog;
  • Still cannot access breed field of object pointed by ppet.
  • cout<<ppet->breed; ILLEGAL : slicing problem
1
2
3
ppet->print(); //Legal
pdog->print();
//Prints the same output if print is defined virtually
  • Because it is virtual, can print breed by calling member function of Dog
  • C++ waits to see what object pointer ppet is actually pointing to before binding call
  • makes pAncestor = pDescendant possible, without losing any data member of pDescendant
  • But needs virtual member functions to access them

Virtual Destructors

1
2
3
4
PFArrayD *p = new PFArrayDBak;
delete p; // Only calls original(or base)class destructor
// if destructor of PFArrayD is virtual, automatically calls PFArrayDBak's destructor
// inside destructor of derived array, it calls base class's destructor
  • Making destructor virtual fixes this
  • Good policy for all destructors to be virtual in this case.
    • If the destructor is marked as virtual, then the destructors for all the derived class are also automatically virtual
  • Pure virtual destructor is possible but must supply its definition (since derived class’s destructor calls base class’s destructor

Base class pointer + virtual function (example)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Shape {
public:
	virtual void draw() = 0;
	virtual double area() = 0;
}; //Abstract base class
class Circle : public Shape {};
class Rectangle : public Shape {};
class Triangle : public Shape {};

vector<Shape*> canvas;
canvas.push_back(new Circle(5));
canvas.push_back(new Rectangle(3, 4));
canvas.push_back(new Triangle(...));

for (Shape* s:canvas) s->draw();

Type Casting (Upcasting)

1
2
3
4
5
Pet vpet;
Dog vdog;
vdog = static_cast<Dog>(vpet); //ILLEGAL (downcasting X)
vpet = vdog; //Legal but slicing problem exists (because vpet is value)
vpet = static_cast<Pet>(vdog); //Legal

Downcasting

  • Assumes that new information from derived class is added
  • Can be done with dynamic_cast
1
2
3
4
5
6
7
Pet *ppet;
ppet = new Dog;
Dog *pDog = dynamic_cast<Dog*>(ppet);
// Legal only for pointers, but dangerous
if (Dog *pDog = dynamic_cast<Dog*>(ppet)) {
	pDog->bark();
} // IF failed, returns nullptr which is zero in number
  • At least 1 member function must be virtual : to check whether type of which Pet* is pointing to
    • Usually makes destructor virtual without making unnecessary virtual function
    • It searches the vtable (to check whether ppet is pointing to dog)
  • dynamic_cast<> returns nullptr if casting is unavailable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Car {
public : 
	virtual void drive() = 0;
};
class Porsche : public Car {
public:
	void driveFast();
};
class Ford : public Car {...}; 

vector <Car*> cars;
cars.push_back(new Porsche());
cars.push_back(new Ford());
Car* porscheAsCar = cars.at(0); 
Porsche* porsche = dynamic_cast<Porsche*>(cars.at(0));
// at least one function in car class must be virtual
porscheAsCar->drive(); // if drive() is declared virtually
porsche->drive(); // not necessary to be virtual this case
porsche->driveFast(); //Legal
porscheAsCar->driveFast(); //Illegal

vttable & vptr

  • Vtable : virtual 함수가 하나라도 있는 class마다 컴파일러가 하나씩 생성
    • 함수 포인터의 배열 : 각 슬롯이 어떤 구현을 가리키는지를 저장.
    • 정적(static), read-only 영역에 위치. → class마다 공유
  • vptr : 객체마다 한개씩 들어있는 숨겨진 멤버 변수(포인터)
    • 본인 클래스의 vtable을 가리킴. 보통 첫 8바이트
    • Base 부분에 들어있는 그 자리를 derived가 덮어서 갱신
    • virtual function이 있는 class의 object에만 생성
1
2
3
4
5
6
7
8
9
class Base {
public:
	virtual void f() { cout<<"Base::f"; }
	virtual void g() { cout<<"Base::g"; }
};
class Derived : public Base {
public:
	void f() override { cout<<"Derived::f"; }
};
  • Vtable for base :
    • [0] → &Base::f
    • [1] → &Base::g
  • Vtable for Derived:
    • [0] → &Derived::f // Override가 vtable의 함수를 override함
    • [1] → &Base::g
  • 객체 메모리 레이아웃 : 같은 위치에 vptr 존재, 하지만 Derived는 derived vtable을 가리킴.
  • 즉 virtual call에서 vptr → vtable → function의 과정이 실제 타입의 dispatch임
1
2
3
4
Base* p = &d;
p-> f(); // virtucal call

(*(p->vptr[0]))(p); //Actually compiler makes
  • p가 가리키는 vptr[0] (함수 f) 를 Dereference하고 p가 이를 호출
    1. p가 가리키는 객체로부터 vptr을 읽음
    2. vptr이 가리키는 vtable의 슬롯[0]을 읽음
    3. 해당 함수의 포인터를 call

→ Base *에 들어있지만, 가리키는 객체의 vptr에 담긴 vtable이 Derived class의 vtable이고, 그 함수의 슬롯이 Derived::f의 함수를 가리키므로 Derived::f가 실행.

→ Late binding의 본체

vptr setting in constructor

  • Derived d;를 실행할 때의 과정
    1. Derived 객체가 들아갈 메모리 할당
    2. Base 부분의 initializer list 실행
    3. Base constructor 본문 진입 직전 : vptr <- &vtable_for_Base : base vtable의 주소를 vptr에 저장
    4. Base 생성자본문 실행 : 이 안에서 일어나는 virtual function call은 Base의 vtable로 dispatched됨
    5. Derived 부분의 initializer list 실행
    6. Derived Constructor 의 본문 진입 직전 : vptr <- &vtable_for_derived : derived vtable의 주소를 vptr에 저장
    7. Derived 생성자 본문 실행 : 여기서 virtual call은 Derived의 vtable로 디스패치
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base {
public:
	Base() { f(); }
	virtual void f()
	{cout<<"Base ";}
};
class Derived : public Base {
	Derived () { f(); }
	void f() override
		{cout<<"Derived ";}
};
int main() {
	Derived d;
}
//Result : Base Derived
  • without vptr : Base constructor called first, then derived constrcutor called
  • with vptr :

  • Destructor : right opposite
    1. Derived 소멸자 본문 실행 : (vptr = derived) → 가상호출은 Derived 버전
    2. 본문 종료 후 : vptr <- &vtable_for_base : 다시 base용으로 돌림
    3. Base 소멸자 본문 실행 → 그 안의 가상 호출은 Base 버전으로 호출
  • Reason : derived 부분이 파괴되었으므로 derived의 가상함수를 부르면 위험

Overhead

  • one vtable per class, 객체당 vptr 8byte (64-bit 기준)
  • vptr → vtable → call : 메모리 로드 2번 추가, 인라인 불가
  • Overhead의 정체.
This post is licensed under CC BY 4.0 by the author.