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가 이를 호출
- p가 가리키는 객체로부터 vptr을 읽음
- vptr이 가리키는 vtable의 슬롯[0]을 읽음
- 해당 함수의 포인터를 call
→ Base *에 들어있지만, 가리키는 객체의 vptr에 담긴 vtable이 Derived class의 vtable이고, 그 함수의 슬롯이 Derived::f의 함수를 가리키므로 Derived::f가 실행.
→ Late binding의 본체
vptr setting in constructor
- Derived d;를 실행할 때의 과정
- Derived 객체가 들아갈 메모리 할당
- Base 부분의 initializer list 실행
- Base constructor 본문 진입 직전 :
vptr <- &vtable_for_Base : base vtable의 주소를 vptr에 저장 - Base 생성자본문 실행 : 이 안에서 일어나는 virtual function call은 Base의 vtable로 dispatched됨
- Derived 부분의 initializer list 실행
- Derived Constructor 의 본문 진입 직전 :
vptr <- &vtable_for_derived : derived vtable의 주소를 vptr에 저장 - 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
- Derived 소멸자 본문 실행 : (vptr = derived) → 가상호출은 Derived 버전
- 본문 종료 후 :
vptr <- &vtable_for_base : 다시 base용으로 돌림 - Base 소멸자 본문 실행 → 그 안의 가상 호출은 Base 버전으로 호출
- Reason : derived 부분이 파괴되었으므로 derived의 가상함수를 부르면 위험
Overhead
- one vtable per class, 객체당 vptr 8byte (64-bit 기준)
- vptr → vtable → call : 메모리 로드 2번 추가, 인라인 불가
- Overhead의 정체.