Imagine that we want to extend our bookstore classes to support several discount strategies. In addition to a bulk discount, we might offer a discount for purchases up to a certain quantity and then charge the full price thereafter. Or we might offer a discount for purchases above a certain limit but not for purchases up to that limit.
Each of these discount strategies is the same in that it requires a quantity and a discount amount. We might support these differing strategies by defining a new class named Disc_quote
to store the quantity and the discount amount. Classes, such as Bulk_item
, that represent a specific discount strategy will inherit from Disc_quote
. Each of the derived classes will implement its discount strategy by defining its own version of net_price
.
Before we can define our Disc_Quote
class, we have to decide what to do about net_price
. Our Disc_quote
class doesn’t correspond to any particular discount strategy; there is no meaning to ascribe to net_price
for this class.
We could define Disc_quote
without its own version of net_price
. In this case, Disc_quote
would inherit net_price
from Quote
.
However, this design would make it possible for our users to write nonsensical code. A user could create an object of type Disc_quote
by supplying a quantity and a discount rate. Passing that Disc_quote
object to a function such as print_total
would use the Quote
version of net_price
. The calculated price would not include the discount that was supplied when the object was created. That state of affairs makes no sense.
Thinking about the question in this detail reveals that our problem is not just that we don’t know how to define net_price
. In practice, we’d like to prevent users from creating Disc_quote
objects at all. This class represents the general concept of a discounted book, not a concrete discount strategy.
We can enforce this design intent—and make it clear that there is no meaning for net_price
—by defining net_price
as a pure virtual function. Unlike ordinary virtuals, a pure virtual function does not have to be defined. We specify that a virtual function is a pure virtual by writing = 0
in place of a function body (i.e., just before the semicolon that ends the declaration). The = 0
may appear only on the declaration of a virtual function in the class body:
// class to hold the discount rate and quantity
// derived classes will implement pricing strategies using these data
class Disc_quote : public Quote {
public:
Disc_quote() = default;
Disc_quote(const std::string& book, double price,
std::size_t qty, double disc):
Quote(book, price),
quantity(qty), discount(disc) { }
double net_price(std::size_t) const = 0;
protected:
std::size_t quantity = 0; // purchase size for the discount to apply
double discount = 0.0; // fractional discount to apply
};
Like our earlier Bulk_item
class, Disc_quote
defines a default constructor and a constructor that takes four parameters. Although we cannot define objects of this type directly, constructors in classes derived from Disc_quote
will use the Disc_quote
constructors to construct the Disc_quote
part of their objects. The constructor that has four parameters passes its first two to the Quote
constructor and directly initializes its own members, discount
and quantity
. The default constructor default initializes those members.
It is worth noting that we can provide a definition for a pure virtual. However, the function body must be defined outside the class. That is, we cannot provide a function body inside the class for a function that is = 0
.
A class containing (or inheriting without overridding) a pure virtual function is an abstract base class. An abstract base class defines an interface for subsequent classes to override. We cannot (directly) create objects of a type that is an abstract base class. Because Disc_quote
defines net_price
as a pure virtual, we cannot define objects of type Disc_quote
. We can define objects of classes that inherit from Disc_quote
, so long as those classes override net_price
:
// Disc_quote declares pure virtual functions, which Bulk_quote will override
Disc_quote discounted; // error: can't define a Disc_quote object
Bulk_quote bulk; // ok: Bulk_quote has no pure virtual functions
Classes that inherit from Disc_quote
must define net_price
or those classes will be abstract as well.
Now we can reimplement Bulk_quote
to inherit from Disc_quote
rather than inheriting directly from Quote
:
// the discount kicks in when a specified number of copies of the same book are sold
// the discount is expressed as a fraction to use to reduce the normal price
class Bulk_quote : public Disc_quote {
public:
Bulk_quote() = default;
Bulk_quote(const std::string& book, double price,
std::size_t qty, double disc):
Disc_quote(book, price, qty, disc) { }
// overrides the base version to implement the bulk purchase discount policy
double net_price(std::size_t) const override;
};
This version of Bulk_quote
has a direct base class, Disc_quote
, and an indirect base class, Quote
. Each Bulk_quote
object has three subobjects: an (empty) Bulk_quote
part, a Disc_quote
subobject, and a Quote
subobject.
As we’ve seen, each class controls the initialization of objects of its type. Therefore, even though Bulk_quote
has no data members of its own, it provides the same four-argument constructor as in our original class. Our new constructor passes its arguments to the Disc_quote
constructor. That constructor in turn runs the Quote
constructor. The Quote
constructor initializes the bookNo
and price
members of bulk
. When the Quote
constructor ends, the Disc_quote
constructor runs and initializes the quantity
and discount
members. At this point, the Bulk_quote
constructor resumes. That constructor has no further initializations or any other work to do.
Adding
Disc_quote
to theQuote
hierarchy is an example of refactoring. Refactoring involves redesigning a class hierarchy to move operations and/or data from one class to another. Refactoring is common in object-oriented applications.It is noteworthy that even though we changed the inheritance hierarchy, code that uses
Bulk_quote
orQuote
would not need to change. However, when classes are refactored (or changed in any other way) we must recompile any code that uses those classes.
Exercises Section 15.4
Exercise 15.15: Define your own versions of
Disc_quote
andBulk_quote
.Exercise 15.16: Rewrite the class representing a limited discount strategy, which you wrote for the exercises in § 15.2.2 (p. 601), to inherit from
Disc_quote
.Exercise 15.17: Try to define an object of type
Disc_quote
and see what errors you get from the compiler.