Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases now! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Hands-On Design Patterns with C++
Hands-On Design Patterns with C++

Hands-On Design Patterns with C++: Solve common C++ problems with modern design patterns and build robust applications , Second Edition

Arrow left icon
Profile Icon Fedor G. Pikus
Arrow right icon
NZ$39.99 NZ$57.99
Full star icon Full star icon Full star icon Full star icon Half star icon 4.5 (11 Ratings)
eBook Jul 2023 626 pages 2nd Edition
eBook
NZ$39.99 NZ$57.99
Paperback
NZ$71.99
Subscription
Free Trial
Arrow left icon
Profile Icon Fedor G. Pikus
Arrow right icon
NZ$39.99 NZ$57.99
Full star icon Full star icon Full star icon Full star icon Half star icon 4.5 (11 Ratings)
eBook Jul 2023 626 pages 2nd Edition
eBook
NZ$39.99 NZ$57.99
Paperback
NZ$71.99
Subscription
Free Trial
eBook
NZ$39.99 NZ$57.99
Paperback
NZ$71.99
Subscription
Free Trial

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
Table of content icon View table of contents Preview book icon Preview Book

Hands-On Design Patterns with C++

An Introduction to Inheritance and Polymorphism

C++ is, first and foremost, an object-oriented language, and objects are the fundamental building blocks of a C++ program. Class hierarchies are used to express relationships and interactions between different parts of a software system, define and implement interfaces between components, and organize data and code. While this isn’t a book for teaching C++, the aim of this chapter is to give the reader enough knowledge about C++ language features as they relate to classes and inheritance, which will be used in later chapters. To that end, we won’t attempt to completely describe the C++ tools for working with classes but introduce the concepts and language constructs that will be used throughout this book.

The following topics will be covered in this chapter:

  • What are classes and what is their role in C++?
  • What are class hierarchies and how does C++ use inheritance?
  • What is runtime polymorphism and how is it used in C++?

Classes and objects

Object-oriented programming is a way to structure a program by combining the algorithms and the data that the algorithms operate on into single entities called objects. Most object-oriented languages, including C++, are class-based. A class is a definition of an object—it describes the algorithms and the data, its format, and its relations to other classes. An object is a concrete instantiation of a class, that is, a variable. An object has an address, which is a location in memory. A class is a user-defined type. In general, any number of objects can be instantiated from the definition provided by the class (some classes limit the number of objects that can be created, but this is an exception, not the norm).

In C++, the data contained in a class is organized as a collection of data members, or variables, of different types. The algorithms are implemented as functions—the methods of the class. While there’s no language requirement that the data members of a class should be somehow relevant to the implementation of its methods, it’s one of the signs of good design when the data is well encapsulated in the classes, and the methods have limited interaction with external data.

This concept of encapsulation is central to the classes in C++—the language allows us to control which data members and methods are public—visible outside of the class, and which are internal—private to the class. A well-designed class has mostly, or only, private data members, and the only public methods are those needed to express the public interface of the class—in other words, what the class does. This public interface is like a contract—the class designer promises that this class provides certain features and operations. The private data and methods of the class are part of the implementation, and they can be changed as long as the public interface, the contract we’ve committed to, remains valid. For example, the following class represents a rational number and supports the increment operation, as exposed by its public interface:

class Rational { public:
  Rational& operator+=(const Rational& rhs);
};

A well-designed class doesn’t expose any more of the implementation details than it has to through its public interface. The implementation isn’t part of the contract, although the documented interface may impose some restrictions on it. For example, if we promise that all rational numbers don’t contain any common multipliers in the numerator and denomination, the addition should include the step of canceling them. That would be a good use of a private member function—the implementation of several other operations will need to call it, but the client of the class never needs to call it because every rational number is already reduced to its lowest terms before it’s exposed to the callers:

class Rational {
  public:
  Rational& operator+=(const Rational& rhs); private:
  long n_; // numerator
  long d_; // denominator
  void  reduce();
};
Rational& Rational::operator+=(const Rational& rhs) {
  n_ = n_*rhs.d_ + rhs.n_*d_;
  d_ = d_*rhs.d_; reduce();
  return *this;
}
Rational a, b; a += b;

The class methods have special access to the data members—they can access the private data of the class. Note the distinction between the class and the object here—operator+=() is a method of the Rational class and is invoked on the object, a. However, it has access to the private data of the b object as well, because a and b are objects of the same class. If a member function references a class member by name without any additional qualifiers, then it’s accessing a member of the same class it’s invoked on (we can make it explicit by writing this->n_ and this->d_). Accessing members of another object of the same class requires a pointer or a reference to that object, but is otherwise not restricted, as would have been the case if we tried to access a private data member from a non-member function.

By the way, C++ also supports C-style structs. But in C++, a struct isn’t limited to just an aggregate of data members—it can have methods, public and private access modifiers, and anything else classes have. From a language point of view, the only difference between a class and a struct is the default access—in a class, all members and methods are private by default, while in a struct they’re public. Beyond that, the use of structs instead of classes is a matter of convention—traditionally, structs are used for C-style structs (structs that would be legal in C) as well as almost C-style structs, for example, a struct with only a constructor added. Of course, this boundary isn’t precise and is a matter of coding styles and practices in each project or team.

In addition to the methods and data members we’ve seen, C++ also supports static data and methods. A static method is very similar to a regular non-member function—it isn’t invoked on any particular object, and the only way it can get access to an object of any type is through its arguments. However, unlike a non-member function, a static method retains its privileged access to the private data of the class.

Classes by themselves are a useful way to group together (encapsulate) the algorithms and the data they operate on and to limit access to some data. However, the most powerful object-oriented features of C++ are inheritance and the resulting class hierarchies.

Inheritance and class hierarchies

Class hierarchies in C++ serve a dual purpose. On the one hand, they allow us to express relations between objects. On the other hand, they let us compose more complex types from simpler ones. Both uses are accomplished through inheritance.

The concept of inheritance is central to the C++ use of classes and objects. Inheritance allows us to define new classes as extensions of existing ones. When a derived class is inherited from the base class, it contains, in some form, all of the data and the algorithms that were in the base class, and it adds some of its own. In C++, it’s important to distinguish between two primary types of inheritance—public and private.

Public inheritance inherits the public interface of the class. It also inherits the implementation—the data members of the base class are also a part of the derived class. But the inheritance of the interface is what distinguishes public inheritance—the derived class has, as a part of its public interface, the public member functions of the base class.

Remember that the public interface is like a contract—we promise to the clients of the class that it supports certain operations, maintains some invariants, and obeys the specified restrictions. By publicly inheriting from the base class, we bind the derived class to the same contract (plus any extensions of the contract, should we decide to define additional public interfaces). Because the derived class also respects the interface contract of the base class, we could use a derived class in any place in the code where a base class is expected—we would not be able to use any of the extensions to the interface (the code expects the base class, we don’t know about any extensions at that point), but the base class interface and its restrictions have to be valid.

This is often expressed as the is-a principle—an instance of a derived class is also an instance of the base class. However, the way we interpret the is-a relationship in C++ isn’t exactly intuitive. For example, is a square a rectangle? If it is, then we can derive the Square class from the Rectangle class:

class Rectangle {
  public:
  double Length() const { return length_; }
  double Width() const { return width_; }
  ...
  private:
  double l_;
  double w_;
};
class Square : public Rectangle {
  ...
};

Right away, there’s something that doesn’t seem right—the derived class has two data members for dimensions, but it really needs only one. We would have to somehow enforce that they’re always the same. This doesn’t seem so bad—the Rectangle class has the interface that allows for any positive values of length and width, and the Square imposes additional restrictions. But it’s worse than that—the Rectangle class has a contract that allows the user to make the dimensions different. This can be quite explicit:

class Rectangle {
  public:
  void Scale(double sl, double sw) {
     // Scale the dimensions
    length_ *= sl;
    width_ *= sw;
  }
  ...
};

Now, we have a public method that allows us to distort the rectangle, altering its aspect ratio. As with any other public method, it’s inherited by the derived classes, so now the Square class has it too. In fact, by using public inheritance, we assert that a Square object can be used anywhere a Rectangle object is used, without even knowing that it’s really a Square. Clearly, this is a promise we can’t keep—when the client of our class hierarchy tries to change the aspect ratio of a square, we can’t do it. We could ignore the call or report an error at runtime. Either way, we’ve violated the contract provided by the base class. There’s only one solution—in C++, a square isn’t a rectangle. Note that a rectangle is usually not a square, either—the contract provided by the Square interface could contain any number of guarantees that we can’t maintain if we derive the Rectangle class from Square.

Similarly, a penguin isn’t a bird in C++ if the bird interface includes flying. The correct design for such cases usually includes a more abstract base class, Bird, that doesn’t make any promises that at least one derived class can’t keep (for example, a Bird object doesn’t make a guarantee that it can fly). Then, we create intermediate-based classes, such as FlyingBird and FlightlessBird, that are derived from the common base class and serve as base classes for the more specific classes such as Eagle or Penguin. The important lesson here is that whether or not a penguin is a bird in C++ depends on how we define what a bird is, or, in C++ terms, what the public interface of the Bird class is.

Because the public inheritance implies the is-a relationship, the language allows a wide range of conversions between references and pointers to different classes in the same hierarchy. First of all, a conversion from a pointer to a derived class into a pointer to the base class is implicit (this is the same for references):

class Base { ... };
class Derived : public Base { ... };
Derived* d = new Derived;
Base* b = d;    // Implicit conversion

This conversion is always valid because an instance of the derived class is also an instance of the base class. The inverse conversion is possible but has to be made explicit:

Base* b = new Derived;     // *b is really Derived
Derived* d = b; // Does not compile, not implicit Derived*
Derived* d1 =
     static_cast<Derived*>(b);    // Explicit conversion

The reason this conversion isn’t implicit is that it’s valid only if the base class pointer really points to a derived object (otherwise, the behavior is undefined). The programmer, therefore, must explicitly assert, using the static cast, that somehow, through the logic of the program or a prior test or by some other means, it’s known that this conversion is valid. If you aren’t sure that the conversion is valid, there’s a safer way to try it without causing undefined behavior; we’ll learn about this in the next section.

Note that the static (or implicit) conversion between pointers to base and derived classes is not quite as straightforward as you might think. The first base of any object always has the same address as the derived object itself, but then it gets more complicated. There is generally no standard requirement on the memory layout of derived classes with multiple bases:

class Base1 { ... };
class Base2 { ... };
class Derived : public Base1, public Base2 { ... };

Most compilers will lay out the base classes first, then the data members of the derived class:

Figure 1.1 – Possible memory layout of a derived class

From Figure 1.1, it is evident that pointer conversion between the base and derived classes generally involves offset calculations. We can easily see this in an example:

// Example 01_cast.C
Derived d;
Derived* p = &d;
std::cout << "Derived: " << (void*)(p) <<
  " Base1: " << (void*)(static_cast<Base1*>(p)) <<
  " Base2: " << (void*)(static_cast<Base2*>(p)) <<
  std::endl;

The program prints something like this:

Derived: 0x7f97e550 Base1: 0x7f97e550 Base2: 0x7f97e560

You can see that the Base1 object is located at the same address as the Derived object, and Base2 starts with an offset (16 bytes, in our case). Seems like the cast is an easy calculation: If you have a pointer to Derived and you want to cast to Base2, add 16. The offsets between base classes are known at compile time, and the compiler knows the layout it is using. Pointer offset calculations are usually implemented in hardware (all modern CPUs support them and do not require a separate addition instruction). This doesn’t sound so hard at all.

Now, what do you do if the pointer is null? The pointer has a value of 0. If you apply the same conversion, you get 16 (0x10), and now your check for null fails:

void f(Base2* p) {
  if (p != nullptr) do_work(*p);
}
Derived* p = nullptr;
f(p); // Will it try to dereference 0x10?

Obviously, this would be very bad, so we can assume that null pointers remain so. Indeed, they do:

Derived* p = nullptr;
std::cout << "Derived: " << (void*)(p) <<
  " Base1: " << (void*)(static_cast<Base1*>(p)) <<
  " Base2: " << (void*)(static_cast<Base2*>(p)) <<
  std::endl;

This prints the same values for all pointers:

Derived: 0x0 Base1: 0x0 Base2: 0x0

This is the only way to do casts, but it implies that a simple implicit cast from Derived* to Base* hides inside a conditional computation with a null pointer check.

The other kind of inheritance in C++ is private inheritance. When inheriting privately, the derived classes don’t extend the public interface of the base class—all base class methods become private in the derived class. Any public interface has to be created by the derived class, starting from a clean slate. There’s no assumption that an object of the derived class can be used in place of an object of the base class. What the derived class does get from the base class is the implementation details—both the methods and the data members can be used by the derived class to implement its own algorithms. It’s said, therefore, that private inheritance implements a has-a relationship—the derived object has an instance of the base class contained inside of it.

The relation of the privately derived class to its base class is, therefore, similar to that of the relationship of a class to its data members. The latter implementation technique is known as composition—an object is composed of any number of other objects, which are all used as its data members. In the absence of any reason to do otherwise, the composition should be preferred to private inheritance. What, then, might be the reasons to use private inheritance? There are several possibilities. First of all, it’s possible, within the derived class, to re-expose one of the public member functions of the base class with the help of a using declaration:

class Container : private std::vector<int> {
  public:
  using std::vector<int>::size;
  ...
};

This can be useful in rare cases, but it’s also equivalent to an inline forwarding function:

class Container {
  private:
  std::vector<int> v_;
  public:
  size_t size() const { return v_.size(); }
  ...
};

Second, a pointer or reference to a derived object can be converted into a pointer or reference to the base object, but only inside a member function of the derived class. Again, the equivalent functionality for composition is provided by taking the address of a data member. So far, we haven’t seen a good reason to use private inheritance, and indeed, the common advice is to prefer composition. But the next two reasons are more significant, and either one could be motivation enough to use private inheritance.

One good reason to use private inheritance has to do with the size of the composed or derived objects. It isn’t uncommon to have base classes that provide only methods but no data members. Such classes have no data of their own and, therefore, should not occupy any memory. But in C++, they have to be given a non-zero size. This has to do with the requirement that any two different objects or variables have different and unique addresses. Typically, if we have two variables declared one after the other, the address of the second one is the address of the first one, plus the size of the first one:

int x;     // Created at address 0xffff0000, size is 4
int y;     // Created at address 0xffff0004

To avoid the need to handle zero-sized objects differently, C++ assigns an empty object the size of one. If such an object is used as a data member of a class, it occupies at least 1 byte (the alignment requirements for the next data member may increase this value). This is wasted memory; it’ll never be used for anything. On the other hand, if an empty class is used as a base class, there’s no requirement that the base part of an object must have a non-zero size. The entire object of the derived class must have a non-zero size, but the address of a derived object, its base object, and its first data member can all be at the same address. Therefore, it’s legal in C++ to allocate no memory for an empty base class, even though sizeof() returns 1 for this class. While legal, such empty base class optimization isn’t required and is considered an optimization. Nonetheless, most modern compilers do this optimization:

class Empty {
  public:
  void useful_function();
};
class Derived : private Empty {
  int i;
};    // sizeof(Derived) == 4
class Composed {
  int i;
  Empty e;
};    // sizeof(Composed) == 8

If we create many derived objects, the memory saved by the empty base optimization can be significant.

The second reason to possibly use private inheritance has to do with virtual functions, and this will be explained in the next section.

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 ...
  }
};

Note that the keyword virtual, when used in a derived class for methods that override base class virtual functions, is entirely optional and has no effect; we will see later that there are good reasons to omit that.

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 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 (this is known as covariant return types).

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 FlyingBird {
  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 (a derived class could also be an abstract class, but then it cannot be instantiated directly either, we must derive another class from it). 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:
  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.

It is also possible to prevent the derived classes from overriding a virtual function by declaring it final:

class Eagle : public FlyingBird {
  public:
  // All Eagles fly the same way, derived classes BaldEagle
  // and GoldenEagle cannot change this.
  void fly(int speed, double direction) final;
};

Note that the use of the final keyword is rare: it is unusual for the design to require that from this point on, the customizations should be disabled in the hierarchy. The final keyword can also be applied to the entire class: it means that no more classes can be derived from this one. Again, this is a rare situation.

So, should or shouldn’t you use the virtual keyword on overrides? This is a matter of style, but the style affects the readability and maintainability of the code. The following is the recommended practice:

  • Any virtual function that does not override one in the base class must use the virtual keyword. This includes both the functions in classes that have no bases and the functions added in derived classes.
  • Any other virtual function should not use the virtual keyword. All overrides should use the override keyword, with the following exception, which is also another rule.
  • A final override must use the final keyword and should not use the override keyword.

There are two advantages to this approach. The first is clarity and readability: if you see virtual, this is a virtual function that does not override anything. If you see override, this must be an override (otherwise the code would not compile). If you see final, this is also an override (again, the code would not compile otherwise) and it’s the last such in the hierarchy. The second advantage shows up during code maintenance. One of the greatest problems with maintaining hierarchies is the base class fragility: you write a set of base and derived classes, someone else comes along and adds an argument to the base class function, and suddenly all your derived class functions don’t override the base class ones and never get called. With consistent use of the override keyword, this will not happen.

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 types, 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 inherited 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 {
  public:
  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::g() 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 were public.

In the previous section, we recommended that 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.

For performance-conscious code, it is important to be aware of the run-time cost of the dynamic cast. Naively, one might think that a virtual function call and a dynamic cast take about the same time: both boil down to the question – is this pointer to Base really a pointer to Derived? A simple benchmark shows that this is not so:

// Example 02_dynamic_cast.C
class Base {
  protected:
  int i = 0;
  public:
  virtual ~Base() {}
  virtual int f() { return ++i; }
};
class Derived : public Base {
  int f() override { return --i; }
};
Derived* p = new Derived;
// Measure the runtime of p->f();
// Measure the runtime of dynamic_cast<Derived*>(p);

The benchmark results should look something like this (the absolute numbers will depend on the hardware): 1 nanosecond for the virtual call, and 5 to 10 nanoseconds for the dynamic cast. Why is the dynamic cast so expensive? We need to learn more about the hierarchies before we can answer this question.

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.

Multiple inheritance

In C++, a class can be derived from several base classes. Going back to our birds, let’s make an observation—while flying birds have a lot in common with each other, they also have something in common with other flying animals, specifically, the ability to fly. Since flight isn’t limited to birds, we may want to move the data and the algorithms related to processing flight into a separate base class. But there’s also no denying that an eagle is a bird. We could express this relation if we used two base classes to construct the Eagle class:

class Eagle : public Bird, public FlyingAnimal { ... };

In this case, the inheritance from both base classes is public, which means that the derived class inherits both interfaces and must fulfill two separate contracts. What happens if both interfaces define a method with the same name? If this method isn’t virtual, then an attempt to invoke it on the derived class is ambiguous, and the program doesn’t compile. If the method is virtual and the derived class has an override for it, then there’s no ambiguity since the method of the derived class is called. Also, Eagle is now both Bird and FlyingAnimal:

Eagle* e = new Eagle;
Bird* b = e;
FlyingAnimal* f = e;

Both conversions from the derived class into the base class pointer are allowed. The reverse conversions must be made explicitly using a static or a dynamic cast. There’s another interesting conversion—if we have a pointer to a FlyingAnimal class that’s also a Bird class, can we cast from one to the other? Yes, we can with a dynamic cast:

Bird* b = new Eagle;   // Also a FlyingAnimal
FlyingAnimal* f = dynamic_cast<FlyingAnimal*>(b);

When used in this context, the dynamic cast is sometimes called a cross-cast—we aren’t casting up or down the hierarchy (between derived and based classes) but across the hierarchy—between the classes on different branches of the hierarchy tree.

Cross-cast is also mostly responsible for the high runtime cost of the dynamic cast we have seen in the previous section. While the most common use of dynamic_cast is to cast from Base* to Derived* to verify that a given object is really of the derived class, the cast could also be used to cast between bases of the same derived class. This is a much harder problem. If you just want to check that the base class object is really a derived one, the compiler knows the Derived type at this point (you cannot use the dynamic cast on incomplete types).

Therefore, the compiler knows exactly what base classes this derived type has and can trivially check if yours is one of them. But when casting across the hierarchy, the compiler knows only two base classes: at the time when this code was written, a derived class that combines both may not exist, it will be written later. But the compiler must generate the correct code now. So, the compiler has to generate code that, at run time, digs through all the possible classes that are derived from both base classes to see if yours is one of them (the actual implementation is less straightforward and more efficient than that, but the task to be accomplished remains the same).

In reality, this overhead is often unnecessary because, most of the time, the dynamic cast is indeed used to find out whether the base class pointer really points to a derived object. In many cases, the overhead is not significant. But if better performance is required, there is no way to make the dynamic cast faster. If you want a fast way to check whether a polymorphic object is really of a given type, you have to use virtual functions and, unfortunately, a list of all possible types (or at least all the ones you might be interested in):

enum type_t { typeBase, typeDerived1, typeDerived2 };
class Base {
  virtual type_t type() const { return typeBase; }
};
class Derived1 : public Base {
  type_t type() const override { return typeDerived1; }
};
…
void process_derived1(Derived1* p);
void do_work(Base* p) {
  if (p->type() == typeDerived1) {
    process_derived1(static_cast<Derived1*>(p));
  }
}

Multiple inheritance is often maligned and disfavored in C++. Much of this advice is outdated and stems from the time when compilers implemented multiple inheritance poorly and inefficiently. Today, with modern compilers, this isn’t a concern. It’s often said that multiple inheritance makes the class hierarchy harder to understand and reason about. Perhaps it would be more accurate to say that it’s harder to design a good multiple inheritance hierarchy that accurately reflects the relations between different properties, and that a poorly designed hierarchy is difficult to understand and reason about.

These concerns mostly apply to hierarchies that use public inheritance. Multiple inheritance can be private as well. There’s even less reason to use multiple private inheritance instead of composition than there was to use single private inheritance. However, the empty base optimization can be done on multiple empty base classes and remains a valid reason to use private inheritance, if it applies:

class Empty1 {};
class Empty2 {};
class Derived : private Empty1, private Empty2 {
  int i;
};   // sizeof(Derived) == 4
class Composed {
  int i;
  Empty1 e1;
  Empty2 e2;
};   // sizeof(Composed) == 8

Multiple inheritance can be particularly effective when the derived class represents a system that combines several unrelated, non-overlapping attributes. We’ll encounter such cases throughout this book when we explore various design patterns and their C++ representations.

Summary

While by no means a complete guide or reference to classes and objects, this chapter introduced and explained the concepts you will need to understand the examples and explanations in the rest of this book. As our interest is and will be in representing design patterns in C++, this chapter focused on the proper use of classes and inheritance. We paid particular attention to what relations are expressed through different C++ features—it’s through these features we’ll express relations and interactions between different components that form a design pattern.

The next chapter will similarly cover knowledge of C++ templates, which will be necessary to understand the subsequent chapters of this book.

Questions

  • What is the importance of objects in C++?
  • Which relation is expressed by public inheritance? Which relation is expressed by private inheritance? What is a polymorphic object?
  • What is the difference between the dynamic cast and the static cast? Why is the dynamic cast so expensive?
Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Delve into the core patterns and components of C++ to master application design
  • Learn tricks, techniques, and best practices to solve common design and architectural challenges
  • Understand the limitation imposed by C++ and how to solve them using design patterns

Description

C++ is a general-purpose programming language designed for efficiency, performance, and flexibility. Design patterns are commonly accepted solutions to well-recognized design problems. In essence, they are a library of reusable components, only for software architecture, and not for a concrete implementation. This book helps you focus on the design patterns that naturally adapt to your needs, and on the patterns that uniquely benefit from the features of C++. Armed with the knowledge of these patterns, you’ll spend less time searching for solutions to common problems and tackle challenges with the solutions developed from experience. You’ll also explore that design patterns are a concise and efficient way to communicate, as patterns are a familiar and recognizable solution to a specific problem and can convey a considerable amount of information with a single line of code. By the end of this book, you’ll have a deep understanding of how to use design patterns to write maintainable, robust, and reusable software.

Who is this book for?

This book is for experienced C++ developers and programmers who wish to learn about software design patterns and principles and apply them to create robust, reusable, and easily maintainable programs and software systems.

What you will learn

  • Recognize the most common design patterns used in C++
  • Understand how to use C++ generic programming to solve common design problems
  • Explore the most powerful C++ idioms, their strengths, and their drawbacks
  • Rediscover how to use popular C++ idioms with generic programming
  • Discover new patterns and idioms made possible by language features of C++17 and C++20
  • Understand the impact of design patterns on the program's performance

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Jul 21, 2023
Length: 626 pages
Edition : 2nd
Language : English
ISBN-13 : 9781804617274
Category :

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning

Product Details

Publication date : Jul 21, 2023
Length: 626 pages
Edition : 2nd
Language : English
ISBN-13 : 9781804617274
Category :

Packt Subscriptions

See our plans and pricing
Modal Close icon
$19.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
$199.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just NZ$7 each
Feature tick icon Exclusive print discounts
$279.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just NZ$7 each
Feature tick icon Exclusive print discounts

Frequently bought together


Stars icon
Total NZ$ 211.97
Hands-On Design Patterns with C++
NZ$71.99
Building Low Latency Applications with C++
NZ$65.99
Expert C++
NZ$73.99
Total NZ$ 211.97 Stars icon

Table of Contents

25 Chapters
Part 1: Getting Started with C++ Features and Concepts Chevron down icon Chevron up icon
Chapter 1: An Introduction to Inheritance and Polymorphism Chevron down icon Chevron up icon
Chapter 2: Class and Function Templates Chevron down icon Chevron up icon
Chapter 3: Memory and Ownership Chevron down icon Chevron up icon
Part 2: Common C++ Idioms Chevron down icon Chevron up icon
Chapter 4: Swap – from Simple to Subtle Chevron down icon Chevron up icon
Chapter 5: A Comprehensive Look at RAII Chevron down icon Chevron up icon
Chapter 6: Understanding Type Erasure Chevron down icon Chevron up icon
Chapter 7: SFINAE, Concepts, and Overload Resolution Management Chevron down icon Chevron up icon
Part 3: C++ Design Patterns Chevron down icon Chevron up icon
Chapter 8: The Curiously Recurring Template Pattern Chevron down icon Chevron up icon
Chapter 9: Named Arguments, Method Chaining, and the Builder Pattern Chevron down icon Chevron up icon
Chapter 10: Local Buffer Optimization Chevron down icon Chevron up icon
Chapter 11: ScopeGuard Chevron down icon Chevron up icon
Chapter 12: Friend Factory Chevron down icon Chevron up icon
Chapter 13: Virtual Constructors and Factories Chevron down icon Chevron up icon
Chapter 14: The Template Method Pattern and the Non-Virtual Idiom Chevron down icon Chevron up icon
Part 4: Advanced C++ Design Patterns Chevron down icon Chevron up icon
Chapter 15: Policy-Based Design Chevron down icon Chevron up icon
Chapter 16: Adapters and Decorators Chevron down icon Chevron up icon
Chapter 17: The Visitor Pattern and Multiple Dispatch Chevron down icon Chevron up icon
Chapter 18: Patterns for Concurrency Chevron down icon Chevron up icon
Assessments Chevron down icon Chevron up icon
Index Chevron down icon Chevron up icon
Other Books You May Enjoy Chevron down icon Chevron up icon

Customer reviews

Most Recent
Rating distribution
Full star icon Full star icon Full star icon Full star icon Half star icon 4.5
(11 Ratings)
5 star 54.5%
4 star 36.4%
3 star 9.1%
2 star 0%
1 star 0%
Filter icon Filter
Most Recent

Filter reviews by




Jun He Oct 09, 2024
Full star icon Full star icon Full star icon Empty star icon Empty star icon 3
This book talks about a lot of good practices and idioms in c++ with history across different versions. The writing can be more concise or better labeled for different versions of C++ when possible. I don't quite follow when some why questions and "really" pop up. I got lost often trying to follow the author's logic on illustrating before C++11, in C++11, 17, 20. Incomplete code snippets are also hard to follow.
Amazon Verified review Amazon
Roman Jul 02, 2024
Full star icon Full star icon Full star icon Full star icon Empty star icon 4
There're a lot of typos and errors in the code snippets in the text of the book, which is unfortunate. Yes, for the most part the problematic code examples are fixed in the github repo, but anyway this makes reading the book very inconvenient.
Subscriber review Packt
N/A Feb 21, 2024
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Feefo Verified review Feefo
E. Leonard Nov 03, 2023
Full star icon Full star icon Full star icon Full star icon Empty star icon 4
This book starts where many other titles aim for the finish line. When the first two chapters of a title are polymorphism and C++ templating you can safely assume it’s not an intro title.The writing style overall is somewhat familiar if you’ve read some packt titles before in that it’s played fairly straight down the middle without resigning to being totally dry and it doesn’t drift into trying to throw loads of hyperbole and forced personality in. It’s a title that will take time as there’s a lot to get through here. The samples and snippets are aimed for digestibility without being entirely trivial.Each subject matter can be, and often is, the subject for entire books itself and after each chapter you’ll find many of these suggested titles that expand on the topics and coverage you’ve just gone through. You’ll also find some questions that could be leading questions in an interview setting to check where you’ll provide a surface answer or dive into some depth. Some of the techniques and patterns are explained as to why they’re relevant for newer versions of C++ showing where they’ve changed or not changed since earlier versions. This gives understanding that the typical reader might be working with C++ codebases that have not updated to a modern version. This stood out across most of the title and was appreciated.All in all a recommended title that gives good reference and re-reading value. It touches on notoriously difficult subject matter with samples that are small enough to make sense without insulting the reader and thoughtful enough to drive home the key takeaways. You’ll need some tenacity in places as it’s not easy content but effort with this title will make you a better engineer on pattern driven codebases. Will absolutely read again.
Amazon Verified review Amazon
Kevin Carpenter Oct 13, 2023
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Design patterns are not generally easy to understand, especially if you’re starting out. However, this book with the way it builds on examples in a top-down approach is about the easiest way of learning them I have found. Additionally, the examples slowly help an intermediate or junior developer start to use more advance techniques quickly. Fedor is a master at performance (just watch some of his talks) and so the whole book has that as a guiding force.The two sections of the book give a nice gentle introduction to some of the basics in C++ with and advance twist. For example, using Templates. I was never a template fan, in general they aren’t difficult once you get a feel for them, but the initial bite can be tough. Fedor’s book makes it far easier, including helping you understand where to apply them.The structure of the book though top-down still allows you to quickly jump to specific sections to learn what you need right now. I like this because on one hand, if working through and advance topic it’s easy to see how things are built off one another, but you can still get where you need quickly. Compared to other C++ books that are more encyclopedia like and have you jumping around, this makes for a more usable text.Fedors examples and explanations are excellent. They are concise, yet easy to follow. If you take the time to work through them, it will improve your understanding of the content.
Amazon Verified review Amazon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

How do I buy and download an eBook? Chevron down icon Chevron up icon

Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.

If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.

Please Note: Packt eBooks are non-returnable and non-refundable.

Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:

  • You may make copies of your eBook for your own use onto any machine
  • You may not pass copies of the eBook on to anyone else
How can I make a purchase on your website? Chevron down icon Chevron up icon

If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:

  1. Register on our website using your email address and the password.
  2. Search for the title by name or ISBN using the search option.
  3. Select the title you want to purchase.
  4. Choose the format you wish to purchase the title in; if you order the Print Book, you get a free eBook copy of the same title. 
  5. Proceed with the checkout process (payment to be made using Credit Card, Debit Cart, or PayPal)
Where can I access support around an eBook? Chevron down icon Chevron up icon
  • If you experience a problem with using or installing Adobe Reader, the contact Adobe directly.
  • To view the errata for the book, see www.packtpub.com/support and view the pages for the title you have.
  • To view your account details or to download a new copy of the book go to www.packtpub.com/account
  • To contact us directly if a problem is not resolved, use www.packtpub.com/contact-us
What eBook formats do Packt support? Chevron down icon Chevron up icon

Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.

You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.

What are the benefits of eBooks? Chevron down icon Chevron up icon
  • You can get the information you need immediately
  • You can easily take them with you on a laptop
  • You can download them an unlimited number of times
  • You can print them out
  • They are copy-paste enabled
  • They are searchable
  • There is no password protection
  • They are lower price than print
  • They save resources and space
What is an eBook? Chevron down icon Chevron up icon

Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.

When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.

For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.