EXPLORATION 37

image

Inheritance

The previous Exploration introduced general OOP principles. Now it’s time to see how to apply those principles to C++.

Deriving a Class

Defining a derived class is just like defining any other class, except that you include a base class access level and name after a colon. See Listing 37-1 for an example of some simple classes to support a library. Every item in the library is a work of some kind: a book, a magazine, a movie, and so on. To keep things simple, the class work has only two derived classes, book and periodical.

Listing 37-1.  Defining a Derived Class

class work
{
public:
  work() = default;
  work(work const&) = default;
  work(std::string const& id, std::string const& title) : id_{id}, title_{title} {}
  std::string const& id()    const { return id_; }
  std::string const& title() const { return title_; }
private:
  std::string id_;
  std::string title_;
};
 
class book : public work
{
public:
  book() : work{}, author_{}, pubyear_{} {}
  book(book const&) = default;
  book(std::string const& id, std::string const& title, std::string const& author,
       int pubyear)
  : work{id, title}, author_{author}, pubyear_{pubyear}
  {}
  std::string const& author() const { return author_; }
  int pubyear()               const { return pubyear_; }
private:
  std::string author_;
  int pubyear_; ///< year of publication
};
 
class periodical : public work
{
public:
  periodical() : work{}, volume_{0}, number_{0}, date_{} {}
  periodical(periodical const&) = default;
  periodical(std::string const& id, std::string const& title, int volume,
             int number,
 std::string const& date)
  : work{id, title}, volume_{volume}, number_{number}, date_{date}
  {}
  int volume()              const { return volume_; }
  int number()              const { return number_; }
  std::string const& date() const { return date_; }
private:
  int volume_;       ///< volume number
  int number_;       ///< issue number
  std::string date_; ///< publication date
};

When you define a class using the struct keyword, the default access level is public. For the class keyword, the default is private. These keywords also affect derived classes. Except in rare circumstances, public is the right choice here, which is what I used to write the classes in Listing 37-1.

Also in Listing 37-1, note that there is something new about the initializer lists. A derived class can (and should) initialize its base class by listing the base class name and its initializer. You can call any constructor by passing the right arguments. If you omit the base class from the initializer list, the compiler uses the base class’s default constructor.

What do you think happens if the base class does not have a default constructor?

_____________________________________________________________

Try it. Change work’s default constructor from = default to = delete and try to compile the code for Listing 37-1. (Add a trivial main(), to ensure that you write a complete program, and be certain to #include all necessary headers.) What happens?

_____________________________________________________________

That’s right; the compiler complains. The exact error message or messages you receive vary from compiler to compiler. I get something like the following:

$ g++ -ansi -pedantic list3701err.cpp
list3701err.cpp: In constructor 'book::book()':
list3701err.cpp:17:41: error: use of deleted function 'work::work()'
   book() : work{}, author_{}, pubyear_{0} {}
                                         ^
list3701err.cpp:4:3: error: declared here
   work() = delete;
   ^
list3701err.cpp: In constructor 'periodical::periodical()':
list3701err.cpp:33:56: error: use of deleted function 'work::work()'
   periodical() : work{}, volume_{0}, number_{0}, date_{} {}
                                                        ^
list3701err.cpp:4:3: error: declared here
   work() = delete;
   ^

Base classes are always initialized before members, starting with the root of the class tree. You can see this for yourself by writing classes that print messages from their constructors, as demonstrated in Listing 37-2.

Listing 37-2.  Printing Messages from Constructors to Illustrate Order of Construction

#include <iostream>
 
class base
{
public:
  base() { std::cout << "base "; }
};
 
class middle : public base
{
public:
  middle() { std::cout << "middle "; }
};
 
class derived : public middle
{
public:
  derived() { std::cout << "derived "; }
};
 
int main()
{
  derived d;
}

What output do you expect from the program in Listing 37-2?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Try it. What output did you actually get?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Were you correct? ________________ In the interest of being thorough, I receive the following:

base
middle
derived

Remember that if you omit the base class from the initializers, or you omit the initializer list entirely, the base class’s default constructor is called. Listing 37-2 contains only default constructors, so what happens is the constructor for derived first invokes the default constructor for middle. The constructor for middle invokes the default constructor for base first, and the constructor for base has nothing to do except execute its function body. Then it returns, and the constructor body for middle executes and returns, finally letting derived run its function body.

Member Functions

A derived class inherits all members of the base class. This means a derived class can call any public member function and access any public data member. So can any users of the derived class. Thus, you can call the id() and title() functions of a book object, and the work::id() and work::title() functions are called.

The access levels affect derived classes, so a derived class cannot access any private members of a base class. (In Exploration 66, you will learn about a third access level that shields members from outside prying eyes while granting access to derived classes.) Thus, the periodical class cannot access the id_ or title_ data members, and so a derived class cannot accidentally change a work’s identity or title. In this way, access levels ensure the integrity of a class. Only the class that declares a data member can alter it, so it can validate all changes, prevent changes, or otherwise control who changes the value and how.

If a derived class declares a member function with the same name as the base class, the derived class function is the only one visible in the derived class. The function in the derived class is said to shadow the function in the base class. As a rule, you want to avoid this situation, but there are several cases in which you very much want to use the same name, without shadowing the base class function. In the next Exploration, you will learn about one such case. Later, you will learn others.

Destructors

When an object is destroyed—perhaps because the function in which it is defined ends and returns—sometimes you have to do some cleanup. A class has another special member function that performs cleanup when an object is destroyed. This special member function is called a destructor.

Like constructors, destructors do not have return values. A destructor name is the class name preceded by a tilde (~). Listing 37-3 adds destructors to the example classes from Listing 37-2.

Listing 37-3.  Order of Calling Destructors

#include <iostream>
 
class base
{
public:
  base()  { std::cout << "base "; }
  ~base() { std::cout << "~base "; }
};
 
class middle : public base
{
public:
  middle()  { std::cout << "middle "; }
  ~middle() { std::cout << "~middle "; }
};
 
class derived : public middle
{
public:
  derived()  { std::cout << "derived "; }
  ~derived() { std::cout << "~derived "; }
};
 
int main()
{
  derived d;
}

What output do you expect from the program in Listing 37-3?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Try it. What do you actually get?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Were you correct? ________________ When a function returns, it destroys all local objects in the reverse order of construction. When a destructor runs, it destroys the most-derived class first, by running the destructor’s function body. It then invokes the immediate base class destructor. Hence, the destructors run in opposite order of construction in the following example.

base
middle
derived
~derived
~middle
~base

If you don’t write a destructor, the compiler writes a trivial one for you. Whether you write your own, or the compiler implicitly writes the destructor, after every destructor body finishes, the compiler arranges to call the destructor for every data member and then execute the destructor for the base classes, starting with the most-derived. For simple classes in these examples, the compiler’s destructors work just fine. Later, you will find more interesting uses for destructors. For now, the main purpose is just to visualize the life cycle of an object.

Read Listing 37-4 carefully.

Listing 37-4.  Constructors and Destructors

#include <iostream>
#include <vector>
 
class base
{
public:
  base(int value) : value_{value} { std::cout << "base(" << value << ") "; }
  base() : base{0} { std::cout << "base() "; }
  base(base const& copy)
  : value_{copy.value_}
  { std::cout << "copy base(" << value_ << ") "; }
 
  ~base() { std::cout << "~base(" << value_ << ") "; }
  int value() const { return value_; }
  base& operator++()
  {
    ++value_;
    return *this;
  }
private:
  int value_;
};
 
class derived : public base
{
public:
  derived(int value): base{value} { std::cout << "derived(" << value << ") "; }
  derived() : base{} { std::cout << "derived() "; }
  derived(derived const& copy)
  : base{copy}
  { std::cout << "copy derived(" << value() << " "; }
  ~derived() { std::cout << "~derived(" << value() << ") "; }
};
 
derived make_derived()
{
  return derived{42};
}
 
base increment(base b)
{
  ++b;
  return b;
}
 
void increment_reference(base& b)
{
  ++b;
}
 
int main()
{
  derived d{make_derived()};
  base b{increment(d)};
  increment_reference(d);
  increment_reference(b);
  derived a(d.value() + b.value());
}

Fill in the left-hand column of Table 37-1 with the output you expect from the program.

Table 37-1. Expected and Actual Results of Running the Program in Listing 37-4

Expected Output

Actual Output

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________
 

Try it. Fill in the right-hand column of Table 37-1 with the actual output and compare the two columns. Did you get everything correct? ________________.

Below is the output generated on my system, along with some commentary. Remember that compilers have some leeway in optimizing away extra calls to the copy constructor. You may get one or two extra copy calls in the mix.

base(42)                       // inside make_derived()
derived(42)                    // finish constructing in make_derived()
copy base(42)                  // copy to b in call to increment()
copy base(43)                  // copy return value from increment to b in main
~base(43)                      // destroy temporary return value
base(87)                       // construct a in main
derived(87)                    // construct a in main
~derived(87)                   // end of main: destroy a
~base(87)                      // destroy a
~base(44)                      // destroy b
~derived(43)                   // destroy d
~base(43)                      // finish destroying d

Note how pass-by-reference (increment_reference) does not invoke any constructors, because no objects are being constructed. Instead, references are passed to the function, and the referenced object is incremented.

By the way, I have not yet shown you how to overload the increment operator, but you probably guessed that’s how it works (in class base). Decrement is similar.

Access Level

At the start of this Exploration, I advised you to use public before the base class name but never explained why. Now is the time to fill you in on the details.

Access levels affect inheritance the same way they affect members. Public inheritance occurs when you use the struct keyword to define a class or use the public keyword before the base class name. Public inheritance means the derived class inherits every member of the base class at the same access level that the members have in the base class. Except in rare circumstances, this is exactly what you want.

Private inheritance occurs when you use the private keyword, and it is the default when you define a class using the class keyword. Private inheritance keeps every member of the base class private and inaccessible to users of the derived class. The compiler still calls the base class constructor and destructor when necessary, and the derived class still inherits all the members of the base class. The derived class can call any of the base class’s public member functions, but no one else can call them through the derived class. It’s as though the derived class re-declares all inherited members as private. Private inheritance lets a derived class make use of the base class without being required to meet the Substitution Principle. This is an advanced technique, and I recommend that you try it only with proper adult supervision.

If the compiler complains about inaccessible members, most likely you forgot to include a public keyword in the class definition. Try compiling Listing 37-5 to see what I mean.

Listing 37-5.  Accidentally Inheriting Privately

class base
{
public:
  base(int v) : value_{v} {}
  int value() const { return value_; }
private:
  int value_;
};
 
class derived : base
{
public:
  derived() : base{42} {}
};
 
int main()
{
  base b{42};
  int x{b.value()};
  derived d{};
  int y{d.value()};
}

The compiler issues an error message, complaining that base is private or not accessible from derived, or something along those lines.

Programming Style

When in doubt, make data members and member functions private, unless and until you know you need to make a member public. Once a member is part of the public interface, anyone using your class is free to use that member, and you have one more code dependency. Changing a public member means finding and fixing all those dependencies. Keep the public interface as small as possible. If you have to add members later, you can, but it’s much harder to remove a member or change it from public to private. Anytime you have to add members to support the public interface, make the supporting functions and data members private.

Use public, not private, inheritance. Remember that inherited members also become part of the derived class’s public interface. If you change which class is the base class, you may have to write additional members in the derived class, to make up for members that were in the original base class but are missing from the new base class. The next Exploration continues the discussion of how derived classes work with base classes to provide important functionality.

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

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