EXPLORATION 38

image

Virtual Functions

Deriving classes is fun, but there’s not a lot you can do with them—at least, not yet. The next step is to see how C++ implements type polymorphism, and this Exploration starts you on that journey.

Type Polymorphism

Recall from Exploration 36 that type polymorphism is the ability of a variable of type B to take the “form” of any class derived from B. The obvious question is: “How?” The key in C++ is to use a magic keyword to declare a member function in a base class and also implement the function in a derived class with a different magic word. The magic keyword tells the compiler that you want to invoke type polymorphism, and the compiler implements the polymorphism magic. Define a variable of type reference-to-base class and initialize it with an object of derived class type. When you call the polymorphic function for the object, the compiled code checks the object’s actual type and calls the derived class implementation of the function. The magic word to turn a function into a polymorphic function is virtual. Derived classes are marked with override.

For example, suppose you want to be able to print any kind of work in the library (see Listing 37-1) using standard (more or less) bibliographical format. For books, I use the format

author, title, year.

For periodicals, I use

title, volume(number), date.

Add a print member function to each class, to print this information. Because this function has different behavior in each derived class, the function is polymorphic, so use the virtual keyword before the base class declaration of print and override after each derived class declaration, as shown in Listing 38-1.

Listing 38-1.  Adding a Polymorphic print Function to Every Class Derived from work

class work
{
public:
  work() = default;
  work(work const&) = default;
  work(std::string const& id, std::string const& title) : id_{id}, title_{title} {}
  virtual ~work() {}
  std::string const& id()    const { return id_; }
  std::string const& title() const { return title_; }
  virtual void print(std::ostream& ) const {}
private:
  std::string id_;
  std::string title_;
};
 
class book : public work
{
public:
  book() : work{}, author_{}, pubyear_{0} {}
  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_; }
  void print(std::ostream& out)
  const override
  {
    out << author() << ", " << title() << ", " << 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_; }
  void print(std::ostream& out)
  const override
  {
    out << title() << ", " << volume() << '(' << number() << "), " <<
           date() << ".";
  }
private:
  int volume_;       ///< volume number
  int number_;       ///< issue number
  std::string date_; ///< publication date
};

image Tip  When writing a stub function, such as print(), in the base class, omit the parameter name or names. The compiler requires only the parameter types. Some compilers warn you if a parameter or variable is not used, and even if the compiler doesn’t issue a warning, it is a clear message to the human who reads your code that the parameters are not used.

A program that has a reference to a work object can call the print member function to print that work, and because print is polymorphic, or virtual, the C++ environment performs its magic to ensure that the correct print is called, depending on whether the work object is actually a book or a periodical. To see this demonstrated, read the program in Listing 38-2.

Listing 38-2.  Calling the print Function

#include <iostream>
#include <string>
 
// All of Listing 38-1 belongs here
... omitted for brevity ...
 
void showoff(work const& w)
{
  w.print(std::cout);
  std::cout << ' ';
}
 
int main()
{
  book sc{"1", "The Sun Also Crashes", "Ernest Lemmingway", 2000};
  book ecpp{"2", "Exploring C++", "Ray Lischner", 2013};
  periodical pop{"3", "Popular C++", 13, 42, "January 1, 2000"};
  periodical today{"4", "C++ Today", 1, 1, "January 13, 1984"};
 
  showoff(sc);
  showoff(ecpp);
  showoff(pop);
  showoff(today);
}

What output do you expect?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Try it. What output do you actually get?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

The showoff function does not have to know about the book or periodical classes. As far as it is concerned, w is a reference to a work object. The only member functions you can call are those declared in the work class. Nonetheless, when showoff calls print, it will invoke book’s print or periodical’s print, if the object’s true type is book or periodical.

Write an output operator (operator<<) that prints a work object by calling its print member function. Compare your solution with my solution, as shown in Listing 38-3.

Listing 38-3.  Output Operator for Class work

std::ostream& operator<<(std::ostream& out, work const& w)
{
  w.print(out);
  return out;
}

Writing the output operator is perfectly normal. Just be certain you declare w as a reference. Polymorphic magic does not occur with ordinary objects, only references. With this operator, you can write any work-derived object to an output stream, and it will print using its print function.

image Tip  The const keyword, if present, always comes before override. Although the specifiers, such as virtual, can be mixed freely with the function’s return type (even if the result is strange, such as int virtual long function()), the const qualifier and override specifier must follow a strict order.

Virtual Functions

A polymorphic function is called a virtual function in C++, owing to the virtual keyword. Once a function is defined as virtual, it remains so in every derived class. The virtual function must have the same name, the same return type, and the same number and type of parameters (but the parameters can have different names) in the derived class.

A derived class is not required to implement a virtual function. If it doesn’t, it inherits the base class function the same way it does for a non-virtual function. When a derived class implements a virtual function, it is said to override the function, because the derived class’s behavior overrides the behavior that would have been inherited from the base class.

In the derived class, the override specifier is optional but helps to prevent mistakes. If you accidentally mistype the function’s name or parameters in the derived class, the compiler might think you are defining a brand-new function. By adding override, you tell the compiler that you intend to override a virtual function that was declared in the base class. If the compiler cannot find a matching function in the base class, it issues an error message.

Add a class, movie, to the library classes. The movie class represents a movie or film recording on tape or disc. Like book and periodical, the movie class derives from work. For the sake of simplicity, define a movie as having an integer running time (in minutes), in addition to the members it inherits from work. Do not override print yet. Compare your class to Listing 38-4.

Listing 38-4.  Adding a Class movie

class movie : public work
{
public:
  movie() : work{}, runtime_{0} {}
  movie(movie const&) = default;
  movie(std::string const& id, std::string const& title, int runtime)
  : work{id, title}, runtime_{runtime}
  {}
  int runtime() const { return runtime_; }
private:
  int runtime_; ///< running length in minutes
};

Now modify the test program from Listing 38-2 to create and print a movie object. If you want, you can take advantage of the new output operator, instead of calling showoff. Compare your program with Listing 38-5.

Listing 38-5.  Using the New movie Class

#include <iostream>
#include <string>
 
// All of Listing 38-1 belongs here
// All of Listing 38-3 belongs here
// All of Listing 38-4 belongs here
... omitted for brevity ...
 
int main()
{
  book sc{"1", "The Sun Also Crashes", "Ernest Lemmingway", 2000};
  book ecpp{"2", "Exploring C++", "Ray Lischner", 2006};
  periodical pop{"3", "Popular C++", 13, 42, "January 1, 2000"};
  periodical today{"4", "C++ Today", 1, 1, "January 13, 1984"};
  movie tr{"5", "Lord of the Token Rings", 314};
 
  std::cout << sc << ' ';
  std::cout << ecpp << ' ';
  std::cout << pop << ' ';
  std::cout << today << ' ';
  std::cout << tr << ' ';
}

What do you expect as the last line of output?

_____________________________________________________________

Try it. What do you get?

_____________________________________________________________

Because movie does not override print, it inherits the implementation from the base class, work. The definition of print in the work class does nothing, so printing the tr object prints nothing.

Fix the problem by adding print to the movie class. Now your movie class should look something like Listing 38-6.

Listing 38-6.  Adding a print Member Function to the movie Class

class movie : public work
{
public:
  movie() : work{}, runtime_{0} {}
  movie(movie const&) = default;
  movie(std::string const& id, std::string const& title, int runtime)
  : work{id, title}, runtime_{runtime}
  {}
  int runtime() const { return runtime_; }
  void print(std::ostream& out)
  const override
  {
    out << title() << " (" << runtime() << " min)";
  }
private:
  int runtime_; ///< running length in minutes
};

The override keyword is optional in the derived class but highly recommended. Some programmers also use the virtual keyword in the derived classes. In C++ 03, this served as a reminder to the human reader that the derived class function overrides a virtual function. The override specifier was added in C++ 11 and has the added feature of telling the compiler the same thing, so the compiler can check your work and complain if you make a mistake. I urge you to use override everywhere it belongs.

EVOLUTION OF A LANGUAGE

You may find it odd that the virtual keyword appears at the start of a function header and override appears at the end. You are witnessing the compromises that are often necessary when a language evolves.

The override specifier is new in C++ 11. One way to add the override specifier would have been to add it to the list of function specifiers, like virtual. But adding a new keyword to a language is fraught with difficulty. Every existing program that uses override as a variable or other user-defined name would break. Programmers all over the world would have to check and possibly modify their software to avoid this new keyword.

So the C++ standard committee devised a way to add override without making it a reserved keyword. The syntax of a function declaration puts the const qualifier in a special place. No other identifiers are allowed there, so it is easy to add override to the syntax for member functions in a manner similar to const, and with no risk of breaking existing code.

Other new language features use existing keywords in new ways, such as =default and =delete for constructors. But a few new keywords were added, and they bring with them the risk of breaking existing code. So the committee tried to choose names that would be less likely to conflict with existing user-chosen names. You will see examples of some of these new keywords later in the book.

References and Slices

The showoff function in Listing 38-2 and the output operator in Listing 38-3 declare their parameter as a reference to const work. What do you expect would happen if you were to change them to pass-by-value?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Try it. Delete the ampersand in the declaration of the output operator, as shown in the following:

std::ostream& operator<<(std::ostream& out, work w)
{
  w.print(out);
  return out;
}

Run the test program from Listing 38-5. What is the actual output?

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Explain what happened.

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

When you pass an argument by value or assign a derived class object to a base class variable, you lose polymorphism. For instance, instead of a book, the result is an honest-to-goodness, genuine, no-artificial-ingredients work—with no memory of book-ness whatsoever. Thus, the output operator ends up calling work’s version of print every time the output operator calls it. That’s why the program’s output is a bunch of empty lines. When you pass a book object to the output operator, not only do you lose polymorphism, but you also lose all sense of book-ness. In particular, you lose the author_ and pubyear_ data members. The data members that a derived class adds are sliced away when the object is copied to a base class variable. Another way to look at it is this: because the derived class members are sliced away, what is left is only a work object, so you cannot have polymorphism. The same thing occurs with assignment.

work w;
book nuts{"7", "C++ in a Nutshell", "Ray Lischner", 2003};
w = nuts; // slices away the author_ and pubyear_; copies only id_ and title_

Slicing is easy to avoid when writing functions (pass all arguments by reference) but harder to cope with for assignment. The techniques you need to manage assignment come much later in this book. For now, I will focus on writing polymorphic functions.

Pure Virtual Functions

The class work defines the print function, but the function doesn’t do anything useful. In order to be useful, every derived class must override print. The author of a base class, such as work, can ensure that every derived class properly overrides a virtual function, by omitting the body of the function and substituting the tokens, = 0, instead. These tokens mark the function as a pure virtual function, which means the function has no implementation to inherit, and derived classes must override the function.

Modify the work class to make print a pure virtual function. Then delete the book class’s print function, just to see what happens. What does happen?

_____________________________________________________________

_____________________________________________________________

The compiler enforces the rules for pure virtual functions. A class that has at least one pure virtual function is said to be abstract. You cannot define an object of abstract type. Fix the program. The new work class should look something like Listing 38-7.

Listing 38-7.  Defining work As an Abstract Class

class work
{
public:
  work() = default;
  work(work const&) = default;
  work(std::string const& id, std::string const& title) : id_(id), title_(title) {}
  virtual ~work() {}
  std::string const& id()    const { return id_; }
  std::string const& title() const { return title_; }
  virtual void print(std::ostream& out) const = 0;
private:
  std::string id_;
  std::string title_;
};

Virtual Destructors

Although most classes you are writing at this time do not require destructors, I want to mention an important implementation rule. Any class that has virtual functions must declare its destructor to be virtual too. This rule is a programming guideline, not a semantic requirement, so the compiler will not help you by issuing a message when you break it (although some compilers may issue a warning). Instead, you must enforce this rule yourself, through discipline.

I will repeat the rule when you begin to write classes that require destructors. If you try any experiments on your own, please be mindful of this rule, or else your programs could be subject to subtle problems—or not-so-subtle crashes.

The next Exploration continues the discussion of classes and their relationship in the C++ type system.

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

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