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
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 Jul 2023
Publisher Packt
ISBN-13 9781804611555
Length 626 pages
Edition 2nd 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 (26) Chapters Close

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

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.

You have been reading a chapter from
Hands-On Design Patterns with C++ - Second Edition
Published in: Jul 2023
Publisher: Packt
ISBN-13: 9781804611555
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