C++ Programming/Classes/Polymorphism
Dynamic polymorphism (Overrides)
[edit | edit source]So far, we have learned that we can add new data and functions to a class through inheritance. But what about if we want our derived class to inherit a method from the base class, but to have a different implementation for it? That is when we are talking about polymorphism, a fundamental concept in OOP programming.
As seen previously in the Programming Paradigms Section, Polymorphism is subdivided in two concepts static polymorphism and dynamic polymorphism. This section concentrates on dynamic polymorphism, which applies in C++ when a derived class overrides a function declared in a base class.
We implement this concept redefining the method in the derived class. However, we need to have some considerations when we do this, so now we must introduce the concepts of dynamic binding, static binding and virtual methods.
Suppose that we have two classes, A
and B
. B
derives from A
and redefines the implementation of a method c()
that resides in class A
. Now suppose that we have an object b
of class B
. How should the instruction b.c()
be interpreted?
If b
is declared in the stack (not declared as a pointer or a reference) the compiler applies static binding, this means it interprets (at compile time) that we refer to the implementation of c()
that resides in B
.
However, if we declare b
as a pointer or a reference of class A
, the compiler could not know which method to call at compile time, because b
can be of type A
or B
. If this is resolved at run time, the method that resides in B
will be called. This is called dynamic binding. If this is resolved at compile time, the method that resides in A
will be called. This is again, static binding.
Virtual member functions
[edit | edit source]The virtual member functions is relatively simple, but often misunderstood. The concept is an essential part of designing a class hierarchy in regards to sub-classing classes as it determines the behavior of overridden methods in certain contexts.
Virtual member functions are class member functions, that can be overridden in any class derived from the one where they were declared. The member function body is then replaced with a new set of implementation in the derived class.
By placing the keyword virtual before a method declaration we are indicating that when the compiler has to decide between applying static binding or dynamic binding it will apply dynamic binding. Otherwise, static binding will be applied.
Again, this should be clearer with an example:
class Foo
{
public:
void f()
{
std::cout << "Foo::f()" << std::endl;
}
virtual void g()
{
std::cout << "Foo::g()" << std::endl;
}
};
class Bar : public Foo
{
public:
void f()
{
std::cout << "Bar::f()" << std::endl;
}
virtual void g()
{
std::cout << "Bar::g()" << std::endl;
}
};
int main()
{
Foo foo;
Bar bar;
Foo *baz = &bar;
Bar *quux = &bar;
foo.f(); // "Foo::f()"
foo.g(); // "Foo::g()"
bar.f(); // "Bar::f()"
bar.g(); // "Bar::g()"
// So far everything we would expect...
baz->f(); // "Foo::f()"
baz->g(); // "Bar::g()"
quux->f(); // "Bar::f()"
quux->g(); // "Bar::g()"
return 0;
}
Our first calls to f() and g() on the two objects are straightforward. However things get interesting with our baz pointer which is a pointer to the Foo type.
f() is not virtual and as such a call to f() will always invoke the implementation associated with the pointer type—in this case the implementation from Foo.
Virtual function calls are computationally more expensive than regular function calls. Virtual functions use pointer indirection, invocation and will require a few extra instructions than normal member functions. They also require that the constructor of any class/structure containing virtual functions to initialize a table of pointers to its virtual member functions.
All this characteristics will signify a trade-off between performance and design. One should avoid preemptively declaring functions virtual without an existing structural need. Keep in mind that virtual functions that are only resolved at run-time cannot be inlined.
Pure virtual member function
[edit | edit source]There is one additional interesting possibility. Sometimes we don't want to provide an implementation of our function at all, but want to require people sub-classing our class to provide an implementation on their own. This is the case for pure virtuals.
To indicate a pure virtual function instead of an implementation we simply add an "= 0" after the function declaration.
Again—an example:
class Widget
{
public:
virtual void paint() = 0;
};
class Button : public Widget
{
public:
void paint() // is virtual because it is an override
{
// do some stuff to draw a button
}
};
Because paint() is a pure virtual function in the Widget class we are required to provide an implementation in all concrete subclasses. If we don't the compiler will give us an error at build time.
This is helpful for providing interfaces—things that we expect from all of the objects based on a certain hierarchy, but when we want to ignore the implementation details.
- So why is this useful?
Let's take our example from above where we had a pure virtual for painting. There are a lot of cases where we want to be able to do things with widgets without worrying about what kind of widget it is. Painting is an easy example.
Imagine that we have something in our application that repaints widgets when they become active. It would just work with pointers to widgets—i.e. Widget *activeWidget() const might be a possible function signature. So we might do something like:
Widget *w = window->activeWidget();
w->paint();
We want to actually call the appropriate paint member function for the "real" widget type—not Widget::paint() (which is a "pure" virtual and will cause the program to crash if called using virtual dispatch). By using a virtual function we insure that the member function implementation for our subclass -- Button::paint() in this case—will be called.
Covariant return types
[edit | edit source]Covariant return types is the ability for a virtual function in a derived class to return a pointer or reference to an instance of itself if the version of the method in the base class does so. e.g.
class base
{
public:
virtual base* create() const;
};
class derived : public base
{
public:
virtual derived* create() const;
};
This allows casting to be avoided.
virtual Constructors
[edit | edit source]There is a hierarchy of classes with base class Foo. Given an object bar belonging in the hierarchy, it is desired to be able to do the following:
- Create an object baz of the same class as bar (say, class Bar) initialized using the default constructor of the class. The syntax normally used is:
- Bar* baz = bar.create();
- Create an object baz of the same class as bar which is a copy of bar. The syntax normally used is:
- Bar* baz = bar.clone();
In the class Foo, the methods Foo::create() and Foo::clone() are declared as follows:
class Foo
{
// ...
public:
// Virtual default constructor
virtual Foo* create() const;
// Virtual copy constructor
virtual Foo* clone() const;
};
If Foo is to be used as an abstract class, the functions may be made pure virtual:
class Foo
{
// ...
public:
virtual Foo* create() const = 0;
virtual Foo* clone() const = 0;
};
In order to support the creation of a default-initialized object, and the creation of a copy object, each class Bar in the hierarchy must have public default and copy constructors. The virtual constructors of Bar are defined as follows:
class Bar : ... // Bar is a descendant of Foo
{
// ...
public:
// Non-virtual default constructor
Bar ();
// Non-virtual copy constructor
Bar (const Bar&);
// Virtual default constructor, inline implementation
Bar* create() const { return new Foo (); }
// Virtual copy constructor, inline implementation
Bar* clone() const { return new Foo (*this); }
};
The above code uses covariant return types. If your compiler doesn't support Bar* Bar::create(), use Foo* Bar::create() instead, and similarly for clone().
While using these virtual constructors, you must manually deallocate the object created by calling delete baz;. This hassle could be avoided if a smart pointer (e.g. std::unique_ptr<Foo>) is used in the return type instead of the plain old Foo*.
Remember that whether or not Foo uses dynamically allocated memory, you must define the destructor virtual ~Foo () and make it virtual to take care of deallocation of objects using pointers to an ancestral type.
virtual Destructor
[edit | edit source]It is of special importance to remember to define a virtual destructor even if empty in any base class, since failing to do so will create problems with the default compiler generated destructor that will not be virtual.
A virtual destructor is not overridden when redefined in a derived class, the definitions to each destructor are cumulative and they start from the last derivate class toward the first base class.
Pure virtual Destructor
[edit | edit source]Every abstract class should contain the declaration of a pure virtual destructor.
Pure virtual destructors are a special case of pure virtual functions (meant to be overridden in a derived class). They must always be defined and that definition should always be empty.
class Interface {
public:
virtual ~Interface() = 0; //declaration of a pure virtual destructor
};
Interface::~Interface(){} //pure virtual destructor definition (should always be empty)