15.8.1. Writing a Basket Class

Image

One of the ironies of object-oriented programming in C++ is that we cannot use objects directly to support it. Instead, we must use pointers and references. Because pointers impose complexity on our programs, we often define auxiliary classes to help manage that complexity. We’ll start by defining a class to represent a basket:

class Basket {
public:
    // Basket uses synthesized default constructor and copy-control members
    void add_item(const std::shared_ptr<Quote> &sale)
        { items.insert(sale); }
    // prints the total price for each book and the overall total for all items in the basket
    double total_receipt(std::ostream&) const;
private:
    // function to compare shared_ptrs needed by the multiset member
    static bool compare(const std::shared_ptr<Quote> &lhs,
                        const std::shared_ptr<Quote> &rhs)
    { return lhs->isbn() < rhs->isbn(); }
    // multiset to hold multiple quotes, ordered by the compare member
    std::multiset<std::shared_ptr<Quote>, decltype(compare)*>
                  items{compare};
};

Our class uses a multiset11.2.1, p. 423) to hold the transactions, so that we can store multiple transactions for the same book, and so that all the transactions for a given book will be kept together (§11.2.2, p. 424).

The elements in our multiset are shared_ptrs and there is no less-than operator for shared_ptr. As a result, we must provide our own comparison operation to order the elements (§11.2.2, p. 425). Here, we define a private static member, named compare, that compares the isbns of the objects to which the shared_ptrs point. We initialize our multiset to use this comparison function through an in-class initializer (§7.3.1, p. 274):

// multiset to hold multiple quotes, ordered by the compare member
std::multiset<std::shared_ptr<Quote>, decltype(compare)*>
              items{compare};

This declaration can be hard to read, but reading from left to right, we see that we are defining a multiset of shared_ptrs to Quote objects. The multiset will use a function with the same type as our compare member to order the elements. The multiset member is named items, and we’re initializing items to use our compare function.

Defining the Members of Basket

The Basket class defines only two operations. We defined the add_item member inside the class. That member takes a shared_ptr to a dynamically allocated Quote and puts that shared_ptr into the multiset. The second member, total_receipt, prints an itemized bill for the contents of the basket and returns the price for all the items in the basket:

double Basket::total_receipt(ostream &os) const
{
    double sum = 0.0;   // holds the running total
    // iter refers to the first element in a batch of elements with the same ISBN
    // upper_bound returns an iterator to the element just past the end of that batch
    for (auto iter = items.cbegin();
              iter != items.cend();
              iter = items.upper_bound(*iter)) {
        // we know there's at least one element with this key in the Basket
        // print the line item for this book
        sum += print_total(os, **iter, items.count(*iter));
    }
    os << "Total Sale: " << sum << endl; // print the final overall total
    return sum;
}

Our for loop starts by defining and initializing iter to refer to the first element in the multiset. The condition checks whether iter is equal to items.cend(). If so, we’ve processed all the purchases and we drop out of the for. Otherwise, we process the next book.

The interesting bit is the “increment” expression in the for. Rather than the usual loop that reads each element, we advance iter to refer to the next key. We skip over all the elements that match the current key by calling upper_bound11.3.5, p. 438). The call to upper_bound returns the iterator that refers to the element just past the last one with the same key as in iter. The iterator we get back denotes either the end of the set or the next book.

Inside the for loop, we call print_total15.1, p. 593) to print the details for each book in the basket:

sum += print_total(os, **iter, items.count(*iter));

The arguments to print_total are an ostream on which to write, a Quote object to process, and a count. When we dereference iter, we get a shared_ptr that points to the object we want to print. To get that object, we must dereference that shared_ptr. Thus, **iter is a Quote object (or an object of a type derived from Quote). We use the multiset count member (§11.3.5, p. 436) to determine how many elements in the multiset have the same key (i.e., the same ISBN).

As we’ve seen, print_total makes a virtual call to net_price, so the resulting price depends on the dynamic type of **iter. The print_total function prints the total for the given book and returns the total price that it calculated. We add that result into sum, which we print after we complete the for loop.

Hiding the Pointers

Users of Basket still have to deal with dynamic memory, because add_item takes a shared_ptr. As a result, users have to write code such as

Basket bsk;
bsk.add_item(make_shared<Quote>("123", 45));
bsk.add_item(make_shared<Bulk_quote>("345", 45, 3, .15));

Our next step will be to redefine add_item so that it takes a Quote object instead of a shared_ptr. This new version of add_item will handle the memory allocation so that our users no longer need to do so. We’ll define two versions, one that will copy its given object and the other that will move from it (§13.6.3, p. 544):

void add_item(const Quote& sale);  // copy the given object
void add_item(Quote&& sale);       // move the given object

The only problem is that add_item doesn’t know what type to allocate. When it does its memory allocation, add_item will copy (or move) its sale parameter. Somewhere there will be a new expression such as:

new Quote(sale)

Unfortunately, this expression won’t do the right thing: new allocates an object of the type we request. This expression allocates an object of type Quote and copies the Quote portion of sale. However, sale might refer to a Bulk_quote object, in which case, that object will be sliced down.

Simulating Virtual Copy
Image

We’ll solve this problem by giving our Quote classes a virtual member that allocates a copy of itself.

class Quote {
public:
    // virtual function to return a dynamically allocated copy of itself
    // these members use reference qualifiers; see §13.6.3 (p. 546)
    virtual Quote* clone() const & {return new Quote(*this);}
    virtual Quote* clone() &&
                        {return new Quote(std::move(*this));}
    // other members as before
};
class Bulk_quote : public Quote {
    Bulk_quote* clone() const & {return new Bulk_quote(*this);}
    Bulk_quote* clone() &&
                   {return new Bulk_quote(std::move(*this));}
    // other members as before
};

Because we have a copy and a move version of add_item, we defined lvalue and rvalue versions of clone13.6.3, p. 546). Each clone function allocates a new object of its own type. The const lvalue reference member copies itself into that newly allocated object; the rvalue reference member moves its own data.

Using clone, it is easy to write our new versions of add_item:

class Basket {
public:
    void add_item(const Quote& sale) // copy the given object
      { items.insert(std::shared_ptr<Quote>(sale.clone())); }
    void add_item(Quote&& sale)      // move the given object
      { items.insert(
          std::shared_ptr<Quote>(std::move(sale).clone())); }
    // other members as before
};

Like add_item itself, clone is overloaded based on whether it is called on an lvalue or an rvalue. Thus, the first version of add_item calls the const lvalue version of clone, and the second version calls the rvalue reference version. Note that in the rvalue version, although the type of sale is an rvalue reference type, sale (like any other variable) is an lvalue (§13.6.1, p. 533). Therefore, we call move to bind an rvalue reference to sale.

Our clone function is also virtual. Whether the Quote or Bulk_quote function is run, depends (as usual) on the dynamic type of sale. Regardless of whether we copy or move the data, clone returns a pointer to a newly allocated object, of its own type. We bind a shared_ptr to that object and call insert to add this newly allocated object to items. Note that because shared_ptr supports the derived-to-base conversion (§15.2.2, p. 597), we can bind a shared_ptr<Quote to a Bulk_quote*.


Exercises Section 15.8.1

Exercise 15.30: Write your own version of the Basket class and use it to compute prices for the same transactions as you used in the previous exercises.


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

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