Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Hands-On Design Patterns with C++

You're reading from   Hands-On Design Patterns with C++ Solve common C++ problems with modern design patterns and build robust applications

Arrow left icon
Product type Paperback
Published in Jan 2019
Publisher Packt
ISBN-13 9781788832564
Length 512 pages
Edition 1st Edition
Languages
Arrow right icon
Author (1):
Arrow left icon
Fedor G. Pikus Fedor G. Pikus
Author Profile Icon Fedor G. Pikus
Fedor G. Pikus
Arrow right icon
View More author details
Toc

Table of Contents (21) Chapters Close

Preface 1. An Introduction to Inheritance and Polymorphism FREE CHAPTER 2. Class and Function Templates 3. Memory Ownership 4. Swap - From Simple to Subtle 5. A Comprehensive Look at RAII 6. Understanding Type Erasure 7. SFINAE and Overload Resolution Management 8. The Curiously Recurring Template Pattern 9. Named Arguments and Method Chaining 10. Local Buffer Optimization 11. ScopeGuard 12. Friend Factory 13. Virtual Constructors and Factories 14. The Template Method Pattern and the Non-Virtual Idiom 15. Singleton - A Classic OOP Pattern 16. Policy-Based Design 17. Adapters and Decorators 18. The Visitor Pattern and Multiple Dispatch 19. Assessments 20. Other Books You May Enjoy

Polymorphism and virtual functions

When we discussed public inheritance earlier, we mentioned that a derived object can be used in any place where a base object is expected. Even with this requirement, it's often useful to know what the actual type of the object is—in other words, what type the object was created as:

Derived d;
Base& b = d;
...
b.some_method(); // b is really a Derived object

some_method() is a part of the public interface of the Base class and has to be valid for the Derived class as well. But, within the flexibility allowed by the contract of the base class interface, it can do something different. As an example, we've already used the avian hierarchy before to represent different birds, in particular, birds that can fly. The FlyingBird class can be assumed to have a fly() method, and every specific bird class derived from it has to support flight. But eagles fly differently from vultures, and so the implementation of the fly() method in the two derived classes, Eagle and Vulture, can be different. Any code that operates on arbitrary FlyingBird objects can call the fly() method, but the results will vary depending on the actual type of the object.

This functionality is implemented in C++ using virtual functions. A virtual public function must be declared in the base class:

class FlyingBird : public Bird {
public:
virtual void fly(double speed, double direction) {
... move the bird at the specified speed in the given direction ...
}
...
};

A derived class inherits both the declaration and the implementation of this function. The declaration and the contract it provides must be respected. If the implementation meets the needs of the derived class, there's no need to do anything more. But if the derived class needs to change the implementation, it can override the implementation of the base class:

class Vulture : public FlyingBird {
public:
virtual void fly(double speed, double direction) {
... move the bird but accumulate exhaustion if too fast ...
}
};

When a virtual function is called, the C++ runtime system must determine what the real type of the object is. Usually, this information isn't known at compile time and must be determined at runtime:

void hunt(FlyingBird& b) {
b.fly(...); // Could be Vulture or Eagle
...
};
Eagle e;
hunt(e); // Now b in hunt() is Eagle, FlyingBird::fly() is called
Vulture v;
hunt(v); // Now b in hunt() is Vulture, Vulture::fly() is called

The programming technique where some code operates on any number of base objects and invokes the same methods, but the results depend on the actual type of these objects, is known as runtime polymorphism, and the objects that support this technique are polymorphic. In C++, polymorphic objects must have at least one virtual function, and only the parts of their interface that use the virtual functions for some or all of the implementation are polymorphic.

It should be evident from this explanation that the declaration of the virtual function and its overrides should be identical—the programmer calls the function on a base object, but the version that's implemented in the derived class runs instead. This can happen only if the two functions have identical arguments and return types (one exception is that if a virtual function in the base class returns a pointer or a reference to an object of some type, the override can return a pointer or a reference to an object derived from that type).

A very common special case of polymorphic hierarchies is one where the base class doesn't have a good default implementation of the virtual function. For example, all flying birds fly, but they all fly at different speeds, so there's no reason to select one speed as the default. In C++, we can refuse to provide any implementation for a virtual function in the base class. Such functions are called pure virtual, and any base class that contains a pure virtual function is known as an abstract class:

class FlyingBirt {
public:
virtual void fly(...) = 0; // Pure virtual function
};

An abstract class defines an interface only; it's the job of the concrete derived classes to implement it. If the base class contains a pure virtual function, every derived class that's instantiated in the program must provide an implementation. In other words, an object of a base class can't be created. We can, however, have a pointer or a reference to an object of a base class—they really point to a derived class, but we can operate on it through the base class interface.

A few notes on the C++ syntax—when overriding a virtual function, it isn't required to repeat the virtual keyword. If the base class declares a virtual function with the same name and arguments, the one in the derived class will always be a virtual function and will override the one from the base class. Note that, if the arguments differ, the derived class function doesn't override anything and instead shadows the name of the base class function. This can lead to subtle bugs where the programmer intended to override a base class function but didn't copy the declaration correctly:

class Eagle : public FlyingBird {
public:
virtual void fly(int speed, double direction);
};

Here, the types of the arguments are slightly different. The Eagle::fly() function is also virtual, but it doesn't override FlyingBird::fly(). If the latter is a pure virtual function, the bug will be caught because every pure virtual function must be implemented in a derived class. But if FlyingBird::fly() has the default implementation, then the bug will go undetected by the compiler. C++11 provides a very useful feature that greatly simplifies finding such bugs—any function that's intended to be an override of a base class virtual function can be declared with the override keyword:

class Eagle : public FlyingBird {
public:
void fly(int speed, double direction) override;
};

The virtual keyword is still optional, but if the FlyingBird class doesn't have a virtual function that we could be overriding with this declaration, this code won't compile.

The most common use of virtual functions, by far, is in hierarchies that use public inheritance—since every derived object is also a base object (is-a relationship), a program can often operate on a collection of derived objects as if they were all of the same type, and the virtual function overrides ensure that the right processing happens for every object:

void MakeLoudBoom(std::vector<FlyingBird*> birds) {
for (auto bird : birds) {
bird->fly(...); // Same action, different results
}
}

But virtual functions can also be used with private inheritance. The use is less straightforward (and much less common)—after all, an object that's derived privately can't be accessed through a base class pointer (a private base class is referred to as an inaccessible base, and an attempt to cast a derived class pointer to the base class will fail). However, there's one context in which this cast is permitted, and that's within a member function of the derived class. Here's, then, the way to arrange a virtual function call from a privately inhered base class to the derived one:

class Base {
public:
virtual void f() { std::cout << "Base::f()" << std::endl; }
void g() { f(); }
};
class Derived : private Base {
virtual void f() { std::cout << "Derived::f()" << std::endl; }
void h() { g(); }
};
Derived d;
d.h(); // Prints "Derived::f()"

The public methods of the Base class become private in the Derived class, so we can't call them directly. We can, however, call them from another method of the Derived class, such as the public method, h(). We can then call f() directly from h(), but that doesn't prove anything—it would come as no surprise if Derived::h() invoked Derived::f(). Instead, we call the Base::f() function that's inherited from the Base class. Inside that function, we're in the Base class—the body of this function may have been written and compiled long before the Derived class was implemented. And yet, in this context, the virtual function override works correctly and Derived::f() is called, just as it would if the inheritance was public.

In the previous section, we recommended that the composition is preferred to private inheritance unless there's a reason to do otherwise. There's no good way to implement similar functionality using composition; so, if the virtual function behavior is desired, private inheritance is the only way to go.

A class with a virtual method has to have its type encoded into every object—this is the only way to know, at runtime, what was the type of the object when it was constructed, after we converted the pointer into a base class pointer and lost any other information about the original type. That type information isn't free; it takes space—a polymorphic object is always larger than an object with the same data members but no virtual methods (usually by the size of a pointer). The extra size doesn't depend on how many virtual functions the class has—at long as it has one, the type information must be encoded in the object. Now, recall that a pointer to the base class can be converted into a pointer to the derived class, but only if we know the correct type of the derived class. With the static cast, there's no way to test whether our knowledge is correct. For non-polymorphic classes (classes without any virtual functions), there can be no better way; once their original type is lost, there is no way to recover it. But for polymorphic objects, the type is encoded in the object, so there has to be a way to use that information to check whether our assumption is correct about which derived object this really is. Indeed, there is a way. It's provided by the dynamic cast:

class Base { ... };
class Derived : public Base { ... };
Base* b1 = new Derived; // Really Derived
Base* b2 = new Base; // Not Derived
Derived* d1 = dynamic_cast<Derived*>(b1); // Succeeds
Derived* d2 = dynamic_cast<Derived*>(b2); // d2 == nullptr

The dynamic cast doesn't tell us what the real type of the object is; rather, it allows us to ask the question—Is the real type Derived? If our guess at the type is correct, the cast succeeds and returns the pointer to the derived object. If the real type is something else, the cast fails and returns a null pointer. The dynamic cast can also be used with references, with similar effects, save one—there's no null reference. A function that returns a reference must always return a reference to some valid object. Since the dynamic cast can't return a reference to a valid object if the requested type doesn't match the actual type. The only alternative is to throw an exception.

So far, we've limited ourselves to only one base class. While it's much easier to think about class hierarchies if we imagine them as trees, with the base class and the root and branches where multiple classes are derived from the same base, C++ doesn't impose such limitations. Next, we'll learn about inheriting from several base classes at once.

You have been reading a chapter from
Hands-On Design Patterns with C++
Published in: Jan 2019
Publisher: Packt
ISBN-13: 9781788832564
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime