Basket
ClassOne 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 multiset
(§11.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_ptr
s 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 isbn
s of the objects to which the shared_ptr
s 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_ptr
s 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.
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_bound
(§11.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_total
(§15.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.
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.
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 clone
(§13.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*
.
3.138.34.226