[C++] Overview of Virtual Functions

Iamcoolz Apr 2, 2012

  1. Iamcoolz

    Iamcoolz Forum Administrator Staff Member XPG Administrator
    0/47

    Joined:
    Mar 30, 2012
    Messages:
    1,227
    Likes Received:
    507
    Trophy Points:
    205
    Gender:
    Male
    Location:
    XPG
    Console:
    Xbox One
    http://www.devarticl...tual-Functions/

    Page 1
    The word virtual is strange enough on its own. It is defined philosophically as "that which is not real" but contains all of the properties of the real object. However, ultimately something virtual is "fake." Virtual functions are similar in concept, so follow along with me through this article to discover how to use them and when to do so in the C++ language.

    This is the second part of my series covering the polymorphic traits in C++ and object-oriented programming, so if you missed it, make sure to catch up with the first part. This article will pick up from where we took a break last time. Just look for Polymorphism in C++ on this web site; start a search or simply click on my name to see my profile and get the link from there.

    Now then, if you managed to follow the first part of this article series, welcome back. We learned what the virtual function itself is, and what direct effect it will have on functions where you declare them with the magic keyword in front of them. To put it as simply as possible, from the point of view of working with the functions where no pointer is implicated, it has no effect whatsoever.

    However, when you assign a derived object to the base pointer, whenever you call a virtual function of the base object, if that function was redefined (what in fact in this case is an overwrite) inside the derived class, the derived class function will be called. While we go through this article, you will learn about abstract classes, pure virtual functions, a method for implementing polymorphic behavior, and ultimately why it is so important for destructors to be virtual.

    We have much to observe and learn, so here we go. Expanding on the knowledge you amassed last week, you will find that, when the function that you call from a base pointer is always going to be decided at run time, the programmers call that dynamic (late) binding. When you call the function from a reference or you call a non-virtual function, that is static binding, and you will not have any polymorphic behavior.

    Page 2
    Abstract classes are the ones that exist only in theory, and there is no way you can perceive an instance of them. Imagine a class hierarchy of the geometrical items, with a common base class under the name GeEntity. From this, we will later form later the circle, line, and so forth.

    If we want to draw all objects on the screen from the internal database, we would like to treat them as generally as we can. To achieve this, we will pass on the objects that need to be drawn via the base pointer and exploit the virtual functions, so inside a draw () member function each class will resolve the draw procedure in its own way.

    The only problem is this: imagine what happens when you pass on, inside this stream, a pointer to a GeEntity. How would you draw a GeEntity on the screen? An entity of itself simply has no meaning. It exists for the sole purpose to allow us to store all of them inside a vector (via the base pointer) and to increase reusability of the code.

    Abstract classes resolve exactly this issue. They can never have an instance, and you can see them as an incomplete derivable (base) class for which the child classes must complete the missing parts; otherwise, they on their own are incomplete (abstract) classes.

    This technique allows us to provide a class from which others can inherit, but at the same time avoid invalid representations. These classes are so generic that some sequences of them first need to get a definition before you can use them.

    This is just like the case of our port with the incoming cargo via the container. A container must exist on a level with some cargo inside it. For example, it may be empty (thus containing air), have a car, boxes with bananas, and so forth. Figuring out how to implement this leads us to the virtual functions. Moreover, it leads us specifically to the pure virtual functions.

    Declaring pure virtual functions will tell/assure the compiler that the function doesn't make sense there, but it will certainly do so in its derivatives, and they will be present when we call the function from the base pointer. Every container will have a cargo with a price (even if it is empty) so I implemented a getPrice () function. It does not makes sense for the function to exist on a container in general, but will have all of its derivatives.
    The routine of declaring a pure virtual function involves assigning the function the zero value, just as I have done inside the BaseContainer:


    Code:
    virtual int getPrice () const = 0;
    The "=0" is the pure specification. As you'll see in the following example, no implementation is allowed or necessary for this function on the BaseContainer. If you try to declare a type of this class, the following error will be thrown:

    Code:
    1>...: error C2259: 'BaseContainer' : cannot instantiate abstract class
    1> due to following members:
    1> 'int BaseContainer::getPrice(void) const' : is abstract

    Page 3
    Now the obvious question comes up as to when to use virtual functions and when to go down the pure virtual road. Virtual functions are already defined in the base class; because of this, they offer a default implementation.

    Suppose you have a collection of derived classes, and a vast majority of them should react in the same manner over a function call. It is a good idea to make the function virtual, so you do not have to write that down repeatedly. The virtual function will be automatically inherited, unless you overwrite it.

    Then again, with the pure virtual function you can avoid the existence of an invalid function/object at a level. As you can see from the previous page, a GeEntity cannot be drawn on the screen, and if we make the draw function a pure virtual, we will assure that we will have only base pointers pointing to derived class objects -- because if you try to declare an abstract class, that will cause an error to be thrown at build time.

    Still, make sure that all of the derived classes will have the function before you use the pure specification. The abstract classes offer a public interface for the base classes rather than a real object. With them, you practically say to the compiler, "trust me, there will be a function present when you call it from the base pointer."

    With an abstract class, pointers and references are possible, so we can take advantage of the polymorphic traits of OOP. Inside an abstract class, you can have as many pure virtual functions as you want, and the members and functions that exist are subject to the inheritance rules. In addition, the class's constructor is called when you declare one of the base classes; it's just that on its own, it cannot exist.

    This scheme allows the programmers to form a layered software system. A perfect example is drivers for operating systems. In our hectic society there are countless devices that all can operate in their own ways. Yet many of the functions used have a certain level of resonance, like their read/write functions.

    It is up to each device as to how it will manipulate the written message that comes in. Maybe it will print a dark picture on the screen, or just start to play the MP3 player you have attached to the computer. Nevertheless, in the midst of all this, the write procedure, from the point of view of the OS, is no different for any driver; it simply places a few bytes of data here and there.

    If you are planning an OOP-oriented OS, this may provide a public interface for all drivers. Then you can leave for the drivers a collection of public virtual (pure virtual) functions that they can just overwrite in order to interpret the data as they want. Now, if you plug in a new device and provide its new driver (a derived object from the public interface), the OS can communicate with the new device even if, at the time the OS was made, the device was not even in the planning stages.

    Page 4
    You've probably heard from more experienced programmers that all deconstructions should be made virtual. The day arrived to learn why this is so crucial. Until now, you may have used only non-virtual destructors.
    If a derived-class object with a non-virtual deconstructor is destroyed explicitly by applying the delete operator to a base-class pointer to the object, the C++ standard specifies that the behavior is undefined.

    The derived class is a base object, but also something more. Destroying only the base makes no sense and can have different results depending on the machine on which it runs. Most probably, you will wake up with a few memory leaks. Wasting memory is against the basic principles of good software engineering.

    The solution is to append the virtual keyword in the front of the destructor. Do this as high as you can in the class hierarchy, preferably in the main root (base) class. As the rule of virtual functions explains, all destructors in the derived class will also be virtual. It is not necessary to make its derived deconstructors virtual, but this will improve readability; even someone from the outside will be able maintain the code with less effort.

    When you delete explicitly with the delete keyword an object using a base pointer to the object, instead of the base pointer's function the derived classes, a deconstructor is responsible for clearing/deleting the object. The constructor will begin from the bottom of the hierarchy, destroy one object, and step further up until it destroys the main base (root) object.

    Here is our class at work. As you can see, we can call without fear the getPrice() function, without implementing it in the base, and not worry that it does not exist:

    Code:
    BananaContainer banana(1, 2, 50);
    CarContainer car(2, "Lamborghini Murcielago", 313000);
    EmptyContainer empty(3);
    
    
    std::vector<BaseContainer*> allContainer;
    allContainer.push_back(&banana);
    allContainer.push_back(&car);
    allContainer.push_back(&empty);
    
    std::vector<BaseContainer*>::iterator it, end;
    for (it = allContainer.begin(), end = allContainer.end();
    it != end; ++it)
    {
    cout << (*it)->getPrice() << endl;
    }
    
    Code:
    100
    313000
    0
    Press any key to continue . . .
    You probably remember the run-time type defining technique we learned in the past article under the name of down casting. Although this is a powerful method, be careful to avoid the cast for each one, and thus lose yourself inside a switch like logic. Today I am going to present another scheme to find out the type of an object at run time. The keyword used is typeid().

    Page 5
    When building the application, the compiler also builds a list of all types and saves the information about them. The typeid() keyword is a function that will return a type_info object that holds all of the information about the type. This function has defined the equality operator, so you can simply check to see if two pointers are pointing to the same type of object at run-time.

    There is only one requirement. The base object must have at least one virtual function, otherwise a double dereference (leading you to the object itself) will point to the class of the object. However, if you have at least a virtual function over there once, you get the following result:

    Code:
    BananaContainer banana(1, 2, 50);
    CarContainer car(2, "Lamborghini Murcielago", 313000);
    EmptyContainer empty(3);
    
    
    std::vector<BaseContainer*> allContainer;
    allContainer.push_back(&banana);
    allContainer.push_back(&car);
    allContainer.push_back(&empty);
    
    std::vector<BaseContainer*>::iterator it, end;
    it = allContainer.begin();
    end = allContainer.end(;
    
    if( typeid(**it) == typeid(**it))
    {
    //this is always true
    }
    
    for (;it != end; ++it)
    {
    cout << endl << typeid(**it).name()<< " " << typeid(**it).raw_name() << endl;
    cout << endl << typeid(*it).name() << " " << typeid( *it).raw_name() << endl;
    }
    
    
    class BananaContainer .?AVBananaContainer@@
    
    class BaseContainer * .PAVBaseContainer@@
    
    class CarContainer .?AVCarContainer@@
    
    class BaseContainer * .PAVBaseContainer@@
    
    class EmptyContainer .?AVEmptyContainer@@
    
    class BaseContainer * .PAVBaseContainer@@


    Press any key to continue . . .
    Before I finish this, there is just one more thing that I would like to share with you. It is about a method to implement the behavior of the virtual keyword for a compiler. This helps you better understand what work is done under the hood and how this is realized.

    For every class that has a virtual function, first construct a virtual table (vtable). This table contains all functions that are virtual and a pointer to the function itself. If that is a pure virtual, then the pointer is NULL.
    Let us examine this with the function below.

    Code:
    (*it)->getPrice();
    First we dereference the "it" iterator so we can access the object to which it is pointing. Follow this with a dereference of the object; now we have an appropriate object. This object begins the vtable with a pointer, so dereference that also.

    Find our function in the table; this is done by passing through the table via an offset value. Once we have the function, call it via the operator() and execute it.

     
  2. Warmonger

    Warmonger Newbie BANNED
    0/47

    Joined:
    Jun 19, 2012
    Messages:
    26
    Likes Received:
    1
    Trophy Points:
    0
    Console:
    Other
    Still no credits? Attaching the source article is unnecessary if you're only going to copy and paste everything here. What's the point of reading it on here, if we can just click the link and read it in its original? :|
     

Share This Page

Close