EXPLORATION 66

image

Multiple Inheritance

Unlike some other object-oriented languages, C++ lets a class have more than one base class. This feature is known as multiple inheritance. Several other languages permit a single base class and introduce a variety of mechanisms for pseudo-inheritance, such as Java interfaces and Ruby mix-ins and modules. Multiple inheritance in C++ is a superset of all these other behaviors.

Multiple Base Classes

Declare more than one base class by listing all the base classes in a comma-separated list. Each base class gets its own access specifier, as demonstrated in the following:

class derived : public base1, private base2, public base3 {
};

As with single inheritance, the derived class has access to all the non-private members of all of its base classes. The derived class constructor initializes all the base classes in order of declaration. If you have to pass arguments to any base class constructor, do so in the initializer list. As with data members, the order of initializers does not matter. Only the order of declaration matters, as illustrated in Listing 66-1.

Listing 66-1.  Demonstrating the Order of Initialization of Base Classes

#include <iostream>
#include <ostream>
#include <string>
 
class visible {
public:
    visible(std::string&& msg) : msg_{std::move(msg)} { std::cout << msg_ << ' '; }
    std::string const& msg() const { return msg_; }
private:
    std::string msg_;
};
 
class base1 : public visible {
public:
   base1(int x) : visible{"base1 constructed"}, value_{x} {}
   int value() const { return value_; }
private:
   int value_;
};
 
class base2 : public visible {
public:
   base2(std::string const& str) : visible{"base2{" + str + "} constructed"} {}
};
 
class base3 : public visible {
public:
   base3() : visible{"base3 constructed"} {}
   int value() const { return 42; }
};
 
class derived : public base1, public base2, public base3 {
public:
   derived(int i, std::string const& str) : base3{}, base2{str}, base1{i} {}
   int value() const { return base1::value() + base3::value(); }
   std::string msg() const
  {
     return base1::msg() + " " + base2::msg() + " " + base3::msg();
  }
};
 
int main()
{
   derived d{42, "example"};
}

Your compiler may issue a warning when you compile the program, pointing out that the order of base classes in derived’s initializer list does not match the order in which the initializers are called. Running the program demonstrates that the order of the base classes controls the order of the constructors, as shown in the following output:

base1 constructed
base2{example} constructed
base3 constructed

Figure 66-1 illustrates the class hierarchy of Listing 66-1. Notice that each of the base1, base2, and base3 classes has its own copy of the visible base class. Don’t be concerned now, but this point will arise later, so pay attention.

9781430261933_Fig66-01.jpg

Figure 66-1. UML diagram of classes in Listing 66-1

If two or more base classes have a member with the same name, you must indicate to the compiler which of them you mean, if you want to access that particular member. Do this by qualifying the member name with the desired base class name when you access the member in the derived class. See the examples in the derived class in Listing 66-1. Change the main() function to the following:

int main()
{
   derived d{42, "example"};
   std::cout << d.value() << ' ' << d.msg() << ' ';
}

Predict the output from the new program.

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Compare your results with the following output I got:

base1 constructed
base2{example} constructed
base3 constructed
84
base1 constructed
base2{example} constructed
base3 constructed

Virtual Base Classes

Sometimes you don’t want a separate copy of a common base class. Instead, you want a single instance of the common base class, and every class shares that one common instance. To share base classes, insert the virtual keyword when declaring the base class. The virtual keyword can come before or after the access specifier; convention is to list it first.

image Note  C++ overloads certain keywords, such as static, virtual, and delete. Virtual base classes have no relationship with virtual functions. They just happen to use the same keyword.

Imagine changing the visible base class to be virtual when each of base1, base2, and base3 derive from it. Can you think of any difficulty that might arise?

_____________________________________________________________

Notice that each of the classes that inherit from visible pass a different value to the constructor for visible. If you want to share a single instance of visible, you have to pick one value and stick with it. To enforce this rule, the compiler ignores all the initializers for a virtual base class, except the one that it requires in the most-derived class (in this case, derived). Thus, to change visible to be virtual, not only must you change the declarations of base1, base2, and base3, but you must also change derived. When derived initializes visible, it initializes the sole, shared instance of visible. Try it. Your modified program should look something like Listing 66-2.

Listing 66-2.  Changing the Inheritance of Visible to Virtual

#include <iostream>
#include <ostream>
#include <string>
 
class visible {
public:
    visible(std::string&& msg) : msg_{std::move(msg)} { std::cout << msg_ << ' '; }
    std::string const& msg() const { return msg_; }
private:
    std::string msg_;
};
 
class base1 : virtual public visible {
public:
   base1(int x) : visible{"base1 constructed"}, value_{x} {}
   int value() const { return value_; }
private:
   int value_;
};
 
class base2 : virtual public visible {
public:
   base2(std::string const& str) : visible{"base2{" + str + "} constructed"} {}
};
 
class base3 : virtual public visible {
public:
   base3() : visible{"base3 constructed"} {}
   int value() const { return 42; }
};
 
class derived : public base1, public base2, public base3 {
public:
   derived(int i, std::string const& str)
   : base3{}, base2{str}, base1{i}, visible{"derived"}
   {}
   int value() const { return base1::value() + base3::value(); }
   std::string msg() const
   {
     return base1::msg() + " " + base2::msg() + " " + base3::msg();
   }
};
 
int main()
{
   derived d{42, "example"};
   std::cout << d.value() << ' ' << d.msg() << ' ';
}

Predict the output from Listing 66-2.

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Notice that the visible class is now initialized only once and that the derived class is the one that initializes it. Thus, every class message is “derived”. This example is unusual because I want to illustrate how virtual base classes work. Most virtual base classes define only a default constructor. This frees authors of derived classes from concerning themselves with passing arguments to the virtual base class constructor. Instead, every derived class invokes the default constructor; it doesn’t matter which class is the most derived.

Figure 66-2 depicts the new class diagram, using virtual inheritance.

9781430261933_Fig66-02.jpg

Figure 66-2. Class diagram with virtual inheritance

Java-Like Interfaces

Programming with interfaces has some important advantages. Being able to separate interfaces from implementations makes it easy to change implementations without affecting other code. If you have to use interfaces, you can easily do so in C++.

C++ has no formal notion of interfaces, but it supports interface-based programming. The essence of an interface in Java and similar languages is that an interface has no data members, and the member functions have no implementations. Recall from Exploration 38 that such a function is called a pure virtual function. Thus, an interface is merely an ordinary class in which you do not define any data members, and you declare all member functions as pure virtual.

For example, Java has the Hashable interface, which defines the hash and equalTo functions. Listing 66-3 shows the equivalent C++ class.

Listing 66-3.  The Hashable Interface in C++

class Hashable
{
public:
   virtual ~Hashable();
   virtual unsigned long hash() const = 0;
   virtual bool equalTo(Hashable const&) const = 0;
};

Any class that implements the Hashable interface must override all the member functions. For example, HashableString implements Hashable for a string, as shown in Listing 66-4.

Listing 66-4.  The HashableString Class

class HashableString : public Hashable
{
public:
   HashableString() : string_{} {}
   ~HashableString() override;
   unsigned long hash() const override;
   bool equalTo(Hashable const&) const override;
 
    // Implement the entire interface of std::string ...
private:
   std::string string_;
};

Note that HashableString does not derive from std::string. Instead, it encapsulates a string and delegates all string functions to the string_ object it holds.

The reason you cannot derive from std::string is the same reason Hashable contains a virtual destructor. Recall from Exploration 38 that any class with at least one virtual function should make its destructor virtual. Let me explain the reason.

To understand the problem, think about what would happen if HashableString were to derive from std::string. Suppose that somewhere else in the program is some code that frees strings (maybe a pool of common strings). This code stores strings as std::string pointers. If HashableString derives from std::string, this is fine. But when the pool object frees a string, it calls the std::string destructor. Because this destructor is not virtual, the HashableString destructor never runs, resulting in undefined behavior. Listing 66-5 illustrates this problem.

Listing 66-5.  Undefined Behavior Arises from HashableString That Derives from std::string

#include <iostream>
#include <istream>
#include <string>
#include <unordered_set>
#include <utility>
 
class Hashable
{
public:
   virtual ~Hashable() {}
   virtual unsigned long hash() const = 0;
   virtual bool equalTo(Hashable const&) const = 0;
};
 
class HashableString : public std::string, public Hashable
{
public:
   HashableString() : std::string{} {}
   HashableString(std::string&& str) : std::string{std::move(str)} {}
   ~HashableString() override {}
 
   unsigned long hash() const override {
      return std::hash<std::string>()(*this);
   }
 
   bool equalTo(Hashable const& s) const override {
      return dynamic_cast<HashableString const&>(s) == *this;
   }
 
};
 
class string_pool
{
public:
   string_pool() : pool_{} {}
   ~string_pool() {
      while (not pool_.empty()) {
         std::string* ptr{ *pool_.begin() };
         pool_.erase(pool_.begin());
         delete ptr;
      }
   }
   std::string* add(std::string&& str) {
      std::string* ptr = new std::string{std::move(str)};
      pool_.insert(ptr);
      return ptr;
   }
private:
   std::unordered_set<std::string*> pool_;
};
 
int main()
{
   string_pool pool{};
   HashableString str{};
   while (std::cin >> str)
   {
      std::cout << "hash of "" << str << "" = " << str.hash() << ' ';
      pool.add(std::move(str));
   }
}

Similarly, if a pool of Hashable pointers were to delete its contents, the std::string destructor would not run. Again, the behavior is undefined. You can at least expect a memory leak, because the memory allocated for the string contents will never be deleted.

On the other hand, if HashableString does not derive from std::string, how can the string pool manage these hashable strings? The short answer is that it cannot. The long answer is that thinking in terms of Java solutions does not work well in C++, because C++ offers a better solution to this kind of problem: templates.

Interfaces vs. Templates

As you can see, C++ supports Java-style interfaces, but that style of programming can lead to difficulties. There are times when Java-like interfaces are the correct C++ solution. There are other situations, however, when C++ offers superior solutions, such as templates.

Instead of writing a HashableString class, write a hash<> class template and specialize the template for any type that has to be stored in a hash table. The primary template provides the default behavior; specialize hash<> for the std::string type. In this way, the string pool can easily store std::string pointers and destroy the string objects properly, and a hash table can compute hash values for strings (and anything else you have to store in the hash table). Listing 66-6 shows one way to write the hash<> class template and a specialization for std::string.

Listing 66-6.  The hash<> Class Template

template<class T>
class hash
{
public:
   std::size_t operator()(T const& x) const
   {
     return reinterpret_cast<std::size_t>(&x);
   }
};
 
template<>
class hash<std::string>
{
public:
   std::size_t operator()(std::string const& str) const
   {
      std::size_t h(0);
      for (auto c : str)
         h = h << 1 | c;
      return h;
   }
};

Now try using the hash<> class template to rewrite the string_pool class. Compare your solution with Listing 66-7.

Listing 66-7.  Rewriting string_pool to Use hash<>

#include <iostream>
#include <istream>
#include <utility>
#include "hash.hpp"        // Listing 66-6
 
#include "string_pool.hpp" // Copied from Listing 66-5
 
int main()
{
   string_pool pool{};
   std::string str{};
   hash<std::string> hasher{};
   while (std::cin >> str)
   {
      std::cout << "hash of "" << str << "" = " << hasher(str) << ' ';
      pool.add(std::move(str));
   }
}

Use the exact same string_pool class as you did in Listing 66-5. The program that uses the string pool is simple and clear and has the distinct advantage of being well-formed and correct. (By the way, the standard library offers std::hash and specializes it for std::string. Trust your library’s implementation to be vastly superior to the toy implementation in this Exploration.)

This approach gives all the functionality of the Hashable interface, but in a manner that allows any type to be hashable without giving up any well-defined behavior. In addition, the hash() function is no longer virtual and can even be an inline function. The speed-up can be considerable if the hash table is accessed in a critical performance path.

Mix-Ins

Another approach to multiple inheritance that you find in languages such as Ruby is the mix-in. A mix-in is a class that typically has no data members, although this is not a requirement in C++ (as it is in some languages). Usually, a C++ mix-in is a class template that defines some member functions that call upon the template arguments to provide input values for those functions.

For example, in Exploration 59, you saw a way to implement assignment in terms of a swap member function. This is a useful idiom, so why not capture it in a mix-in class, so you can easily reuse it? The mix-in class is actually a class template that takes a single template argument: the derived class. The mix-in defines the assignment operator, using the swap function that the template argument provides.

Confused yet? You aren’t alone. This is a common idiom in C++, but one that takes time before it becomes familiar and natural. Listing 66-8 helps to clarify how this kind of mix-in works.

Listing 66-8.  The assignment_mixin Class Template

template<class T>
class assignment_mixin {
public:
   T& operator=(T rhs)
   {
      rhs.swap(static_cast<T&>(*this));
      return static_cast<T&>(*this);
   }
};

The trick is that instead of swapping *this, the mix-in class casts itself to a reference to the template argument, T. In this way, the mix-in never has to know anything about the derived class. The only requirement is that the class, T, must be copyable (so it can be an argument to the assignment function) and have a swap member function.

In order to use the assignment_mixin class, derive your class from the assignment_mixin (as well as any other mix-ins you wish to use), using the derived class name as the template argument. Listing 66-9 shows an example of how a class uses mix-ins.

Listing 66-9.  Using mix-in Class Template

#include <string>
#include <utility>
#include "assignment_mixin.hpp" // Listing 66-8
 
class thing: public assignment_mixin<thing> {
public:
   thing() : value_{} {}
   thing(std::string&& s) : value_{std::move(s)} {}
   void swap(thing& other) { value_.swap(other.value_); }
private:
   std::string value_;
};
 
int main()
{
   thing one{};
   thing two{"two"};
   one = two;
}

This C++ idiom is hard to comprehend at first, so let’s break it down. First, consider the assignment_mixin class template. Like many other templates, it takes a single template parameter. It defines a single member function, which happens to be an overloaded assignment operator. There’s nothing particularly special about assignment_mixin.

But assignment_mixin has one important property: the compiler can compile the template even if the template argument is an incomplete class. The compiler doesn’t have to expand the assignment operator until it is used, and at that point, T must be complete. But for the class itself, T can be incomplete. If the mix-in class were to declare a data member of type T, then the compiler would require that T be a complete type when the mix-in is instantiated, because it would have to know the size of the mix-in.

In other words, you can use assignment_mixin as a base class, even if the template argument is an incomplete class.

When the compiler processes a class definition, immediately upon seeing the class name, it records that name in the current scope as an incomplete type. Thus, when assignment_mixin<thing> appears in the base class list, the compiler is able to instantiate the base class template using the incomplete type, thing, as the template argument.

By the time the compiler gets to the end of the class definition, thing becomes a complete type. After that, you will be able to use the assignment operator, because when the compiler instantiates that template, it needs a complete type, and it has one.

Protected Access Level

In addition to the private and public access levels, C++ offers the protected access level. A protected member is accessible only to the class itself and to derived classes. To all other would-be users, a protected member is off-limits, just like private members.

Most members are private or public. Use protected members only when you are designing a class hierarchy and you deliberately want derived classes to call a certain member function but don’t want anyone else to call it.

Mix-in classes sometimes have a protected constructor. This ensures that no one tries to construct a stand-alone instance of the class. Listing 66-10 shows assignment_mixin with a protected constructor.

Listing 66-10.  Adding a Protected Constructor to the assignment_mixin Class Template

template<class T>
class assignment_mixin {
public:
   T& operator=(T rhs)
   {
      rhs.swap(static_cast<T&>(*this));
      return static_cast<T&>(*this);
   }
protected:
  assignment_mixin() {}
};

Multiple inheritance also appears in the C++ standard library. You know about istream for input and ostream for output. The library also has iostream, so a single stream can perform input and output. As you might expect, iostream derives from istream and ostream. The only quirk has nothing to do with multiple inheritance: iostream is defined in the <istream> header. The <iostream> header defines the names std::cin, std::cout, etc. The header name is an accident of history.

The next Exploration continues your advanced study of types, by looking at policies and traits.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset