Principles and Details of C++ Polymorphism

Keywords: C++

Catalog

  • Definition of polymorphism
  • Compile-time polymorphism
  • Runtime Polymorphism
  • Implementation principle of virtual function
  • Instance analysis of mechanism of virtual function table

What is polymorphism

Polymorphic C++ is one of the three main features of object-oriented (OOP) language (the other two are encapsulation and inheritance)

Polymorphisms are simply summarized as "one interface, many methods".It refers to the different actions taken by the same object when it receives different messages or when different objects receive the same message.

Polymorphism has three important components:
1. Same function name
2. Context-dependent
3. Different implementation mechanisms

The advantage of polymorphism is that different tasks are distributed using as few function names as possible, and contextual information is used to determine how they are implemented.

Implementation of polymorphism: function overload, template function and virtual function

Compile-time polymorphism

A polymorphism is defined at compile time and determines how a function with the same function name is called using parameters.Includes function overloads and template functions

  1. Function overloading: Functions with different parameters are allowed to have the same name, primarily by renaming them from different parameter lists to achieve polymorphism.So functions that overload must have different parameter lists.One problem, however, is that it cannot overload the return values.A function template is needed to complete the polymorphism.
  2. Template function: A custom type template is used to define a function, that is, the parameter type of the function parameter is undetermined, but the number of parameters of the function is determined.Using function templates alone is not polymorphic, because although different types of the same function calls are implemented, the same template function definition is still used.The core needs to use the functionality of template specialization to specialize functions that need to be polymorphic to achieve polymorphism.Tip: Template functions can be overloaded just like normal functions to fully reflect polymorphism.

Runtime Polymorphism

Functions are functions that can only be determined during the execution of a program to be actually executed.

Key concepts

Virtual function: The implementation allowed in a derived class that exists in a base class is different from the base class.Represented by virtual keyword.

Virtual function table pointer: A pointer added by the compiler to the virtual function address table for each class with virtual functions.

Virtual function table: A table that records the addresses of virtual functions currently inherited and customized.

Subtypes: If type X extends or implements type Y, then X is a subtype of Y. Note: The difference between subtypes and inheritance, subtypes are used to express interface compatibility. When B is a subtype of A, it means that all operations on A can be performed on B. Inheritance tends to express the reuse of implementation, that is, the operation of B reusing A.Do to achieve their own operations.

Transition up: Convert an object of a subtype to an object of a parent type.In the process of upward transformation, there are three points to pay attention to.It is safe to shift up.Second, the upward transition can be completed automatically.Third, subtype information will be lost in the process of upward transformation.

Implementation of polymorphism

A function declared in the base class to be the virtual keyword is overloaded by the derived class, which implements polymorphism by calling the virtual function content implemented by the derived class when pointing to the derived class with the base class pointer.

Why can't runtime polymorphisms be determined at compile time?

You never know what type of object is provided in the run, only you can use the base class pointer to point to it.

//For example, for this function, the direction of the base class pointer is determined only by code, while the compilation time cannot determine the direction of the base class pointer for this function. Only at runtime, for each given code, the corresponding derived class object is dynamically returned.
base* do_something(int code) {
    switch(code){
        case 1:
            return new A();
        case 2:
            return new B();
        default;
            return nullptr;
    }
}

Why pointers or references are needed to implement runtime polymorphism

Consider the following functions

class b{
public:
    virtual void print(){cout<<"here is the base"<<endl;}
};
class b1: public b{
public:
    void print(){cout<<"here is the b1"<<endl;}
};
class b2:public b{
public:
    void print(){cout<<"here is the b2"<<endl;}
};
void print_result(b a){
    a.print();
}
void print_result_ref(b& a){
    a.print();
}
int main() {
    b1 a;
    b2 b;
    print_result(a);
    print_result(b);
    print_result_ref(a);
    print_result_ref(b);
}

The output is:

here is the base
here is the base
here is the b1
here is the b2

When you go to a copy of a derived class object, you get the contents of the base class virtual function. While it is clear that the derived class redefines the base class virtual function, you should return the contents of the derived class. In fact, there is an upward transition of subtypes, which implicitly converts the derived class to the object of the base class and deletes the derived class.The contents of the superfluous base class so that only the functions and other contents of the base class can be called.When using a reference, C++ runs with a base class pointer to the derived class object, and gets the address of the real virtual function from the virtual function table lookup table, resulting in the output of the derived class.

Why do destructors need to be written as virtual functions?

To prevent memory leaks, when the base class pointer is used to point to a derived class object, the process of destroying the object only calls the destructor of the base class, so there will be values that destruct the content of the base class, not the exclusive content of the derived class.Destructor.

Implementation principle of virtual function

With the function table pointer pointing to the starting position of a virtual function table, the virtual function table stores the virtual function pointer in this class, the virtual function table pointer can be found when calling, the virtual function table pointer can be found through the virtual function table pointer, the function entrance can be found by using the offset of the virtual function table, and the virtual function to be used can be found.
When instantiating a subclass object of a class, if there is no virtual function defined in the subclass and the virtual function of the parent class is inherited, the subclass object will also have a virtual function table with the same function address as the parent class.If the subclass defines a virtual function inherited from the parent, the subclass overrides the function pointer of the parent class

Analysis of the Action Mechanism of Virtual Function Table

//base class
class base{
public:
    base(long m1 = 1, long m2 = 2):m1(m1),m2(m2){};
    virtual void virtualbase1() {
        std::cout<<"this is the base1 vitual funciton"<<endl;
    }
    virtual void virtualbase2() {
        std::cout<<"this is the base2 vitual funciton"<<endl;
    }
    virtual void virtualbase4() {
        std::cout<<"this is the base3 vitual funciton"<<endl;
    }
private:
    long m1;
    long m2;
};
//Derived Class
class base1 : public base{
    void virtualbase2() {
        std::cout<<"this is the base from subclass virtual function"<<endl;
    }
};

The base class contains two variables m1,m2, and three virtual functions
Re-implementation of a class virtual function in a derived class

int main(){

    base b;

    long * bAddress = (long *)&b;

    //Address of output base class
    cout<<"bAddress: "<<bAddress<<endl;
    //Address of output virtual table pointer
    long * vtptr = (long *)*(bAddress + 0);
    cout<<"\t vtptr:"<<vtptr<<endl;
    //Address of the first virtual function in the output virtual table
    long *pfunc1 = (long *)*(vtptr + 0);
    cout<<"\t vfunc1:"<<pfunc1<<endl;
    //The address of the second virtual function in the output virtual table
    long *pfunc2 = (long *)*(vtptr + 1);
    cout<<"\t vfunc2:"<<pfunc2<<endl;
    //The address of the third virtual function in the output virtual table
    long *pfunc3 = (long *)*(vtptr + 2);
    cout<<"\t vfunc3:"<<pfunc3<<endl;
    //Getting the content of private object m1 using the object's address offset
    long m1 = (long)*(bAddress + 1);
    cout<<"m1: "<<m1<<endl;
    //Similarly, get the content of private object m2
    long m2 = (long)*(bAddress + 2);
    cout<<"m2: "<<m2<<endl;

    //Convert pointer to function pointer
    void (* pfunc1_fun)() = (void (*)()) pfunc1;
    //Get the result of the function
    pfunc1_fun();

    //New Derived Class Object b1
    base1 b1;
    //Get the address of the object
    long * b1Address = (long *) &b1;
    cout<<"b1Address: "<<b1Address<<endl;
    //Get the address of the virtual table pointer
    long * b1vtptr = (long *)*(b1Address + 0);
    cout<<"\t b1vtptr: "<<b1vtptr<<endl;
    //Gets the address of a function in a virtual table
    long * b1pfunc1 = (long *)*(b1vtptr + 0);
    cout<<"\t b1pfunc1: "<<b1pfunc1<<endl;
    //Gets the address of a function in a virtual table
    long * b2pfunc2 = (long *)*(b1vtptr + 1);
    cout<<"\t b1pfunc2: "<<b2pfunc2<<endl;
    //Gets the address of a function in a virtual table
    long * b3pfunc3 = (long *)*(b1vtptr + 2);
    cout<<"\t b1pfunc3: "<<b3pfunc3<<endl;
    //Convert pointer to function pointer
    void(* b2pfunc2_fun)() = (void(*)()) b2pfunc2;
    //Get the result of the function
    b2pfunc2_fun();
    return 0;
}

Run these functions to get the results:

bAddress: 0x7ffeeb90f9f0
     vtptr:0x1042f7528
     vfunc1:0x1042f3a50
     vfunc2:0x1042f3a90
     vfunc3:0x1042f3ad0
m1: 1
m2: 2
this is the base1 vitual funciton
b1Address: 0x7ffeeb90f998
     b1vtptr: 0x1042f7560
     b1pfunc1: 0x1042f3a50
     b1pfunc2: 0x1042f3b60
     b1pfunc3: 0x1042f3ad0
this is the base from subclass virtual function

The result shows that when a derived class redefines a function of the base class, the pointer in its virtual function table overrides, and where it is not redefined, the address of the virtual function of the base class is maintained.

Posted by HFD on Wed, 21 Aug 2019 19:06:50 -0700