15.8.1. Разработка класса Basket
Ирония объектно-ориентированного программирования на языке С++ в том, что невозможно использовать объекты непосредственно. Вместо них приходится использовать указатели и ссылки. Поскольку указатели усложняют программы, зачастую приходится определять вспомогательные классы, чтобы избежать осложнений. Для начала определим класс, представляющий корзину покупателя:
class Basket {
public:
// Basket использует синтезируемый стандартный конструктор и
// функции-члены управления копированием
void add_item(const std::shared_ptr<Quote> &sale)
{ items.insert(sale); }
// выводит общую стоимость каждой книги и общий счет для всех
// товаров в корзинке
double total_receipt(std::ostream&) const;
private:
// функция сравнения shared_ptr, необходимая элементам
// набора multiset
static bool compare(const std::shared_ptr<Quote> &lhs,
const std::shared_ptr<Quote> &rhs)
{ return lhs->isbn() < rhs->isbn(); }
// набор multiset содержит несколько стратегий расценок,
// упорядоченных по сравниваемому элементу
std::multiset<std::shared_ptr<Quote>, decltype(compare)*>
items{compare};
}
Для хранения транзакций класс использует контейнер multiset (см. раздел 11.2.1), позволяющий содержать несколько транзакций по той же книге, чтобы все транзакции для данной книги находились вместе (см. раздел 11.2.2).
Элементами контейнера multiset будут указатели shared_ptr, и для них нет оператора "меньше". В результате придется предоставить собственный оператор сравнения для упорядочивания элементов (см. раздел 11.2.2). Здесь определяется закрытая статическая функция-член compare(), сравнивающая isbn объектов, на которые указывают указатели shared_ptr. Инициализируем контейнер multiset с использованием этой функции сравнения и внутриклассового инициализатора (см. раздел 7.3.1):
// набор multiset содержит несколько стратегий расценок,
// упорядоченных по сравниваемому элементу
std::multiset<std::shared_ptr<Quote>, decltype(compare)*>
items{compare};
Это объявление может быть трудно понять, но, читая его слева направо, можно заметить, что определяется контейнер multiset указателей shared_ptr на объекты класса Quote. Для упорядочивания элементов контейнер multiset будет использовать функцию с тем же типом, что и функция-член compare(). Элементами контейнера multiset будут объекты items, которые инициализируются для использования функции compare().
Определение членов класса Basket
Класс Basket определяет только две функции. Функция-член add_item() определена в классе. Она получает указатель shared_ptr на динамически созданный объект класса Quote и помещает его в контейнер multiset. Вторая функция-член, total_receipt(), выводит полученный счет для содержимого корзины и возвращает цену за все элементы в ней:
double Basket::total_receipt(ostream &os) const {
double sum = 0.0; // содержит текущую сумму
// iter ссылается на первый элемент в пакете элементов с тем же ISBN
// upper_bound() возвращает итератор на элемент сразу после
// конца этого пакета
for (auto iter = items.cbegin();
iter != items.cend();
iter = items.upper_bound(*iter)) {
// известно, что в Basket есть по крайней мере один элемент
// с этим ключом
// вывести строку для элемента этой книги
sum += print_total(os, **iter, items.count(*iter));
}
os << "Total Sale: " << sum << endl; // вывести в конце общий счет
return sum;
}
Цикл for начинается с определения и инициализации итератора iter на первый элемент контейнера multiset. Условие проверяет, не равен ли iter значению items.cend(). Если да, то обработаны все покупки и цикл for завершается. В противном случае обрабатывается следующая книга.
Интересный момент — выражение "инкремента" в цикле for. Это не обычный цикл, читающий каждый элемент и перемещающий итератор iter на следующий. При вызове функции upper_bound() (см. раздел 11.3.5) он перескакивает через все элементы, которые соответствуют текущему ключу. Вызов функции upper_bound() возвращает итератор на элемент сразу после последнего с тем же ключом, что и iter. Возвращаемый итератор обозначает или конец набора, или следующую книгу.
Для вывода подробностей по каждой книге в корзине в цикле for происходит вызов функции print_total() (см. раздел 15.1):
sum += print_total(os, **iter, items.count(*iter));
Аргументами функции print_total() являются поток ostream для записи, обрабатываемый объект Quote и счет. При обращении к значению итератора iter возвращается указатель shared_ptr, указывающий на объект, который предстоит вывести. Чтобы получить этот объект, следует обратиться к значению этого указателя shared_ptr. Таким образом, выражение **iter возвращает объект класса Quote (или класса производного от него). Для выяснения количества элементов в контейнере multiset с тем же ключом (т.е. с тем же ISBN) используется его функция-член count() (см. раздел 11.3.5).
Как уже упоминалось, функция print_total() осуществляет вызов виртуальной функции net_price(), поэтому полученная цена зависит от динамического типа **iter. Функция print_total() выводит общую сумму для данной книги и возвращает вычисленную общую стоимость. Результат добавляется в переменную sum, которая выводится после завершения цикла for.
Сокрытие указателей
Пользователи класса Basket все еще должны иметь дело с динамической памятью, поскольку функция add_item() получает указатель shared_ptr. В результате пользователи вынуждены писать код так:
Basket bsk;
bsk.add_item(make_shared<Quote>("123", 45));
bsk.add_item(make_shared<Bulk_quote>("345", 45, 3, .15));
На следующем этапе переопределим функцию add_item() так, чтобы она получала объект класса Quote вместо указателя shared_ptr. Эта новая версия функции add_item() отработает резервирование памяти так, чтобы пользователи больше не должны были делать это сами. Определим две ее версии: одна будет копировать переданный ей объект, а другая перемещать его (см. раздел 13.6.3):
void add_item(const Quote& sale); // копирует переданный объект
void add_item(Quote&& sale); // перемещает переданный объект
Единственная проблема в том, что функция add_item() не знает, какой тип резервировать. При резервировании памяти функция add_item() скопирует (или переместит) свой параметр sale. Выражение new будет выглядеть примерно так:
new Quote(sale)
К сожалению, это выражение будет неправильным: оператор new резервирует объект запрошенного типа. Оно резервирует объект типа Quote и копирует часть Quote параметра sale. Но если переданный параметру sale объект будет иметь тип Bulk_quote, то он будет усечен.
Эту проблему можно решить, снабдив класс Quote виртуальной функцией-членом, резервирующей его копию.
class Quote {
public:
// виртуальная функция, возвращающая динамически созданную копию
// эти члены используют квалификаторы ссылки; раздел 13.6.3
virtual Quote* clone() const & {return new Quote(*this);}
virtual Quote* clone() &&
{return new Quote(std::move(*this));}
// другие члены как прежде
};
class Bulk_quote : public Quote {
Bulk_quote* clone() const & {return new Bulk_quote(*this);}
Bulk_quote* clone() &&
{return new Bulk_quote(std::move(*this));}
// другие члены, как прежде
};
Поскольку функция add_item() имеет версии копирования и перемещения, были определены версии l- и r-значения функции clone() (см. раздел 13.6.3). Каждая функция clone() резервирует новый объект ее собственного типа. Функция-член константной ссылки на l-значение копирует себя во вновь зарезервированный объект; функция-член ссылки на r-значение перемещает свои данные.
Используя функцию clone(), довольно просто написать новые версии функции add_item():
class Basket {
public:
void add_item(const Quote& sale) // копирует переданный объект
{ items.insert(std::shared_ptr<Quote>(sale.clone())); }
void add_item(Quote&& sale) // перемещает переданный объект
{ items.insert(
std::shared_ptr<Quote>(std::move(sale).clone())); }
// другие члены, как прежде
};
Как и сама функция add_item(), функция clone() перегружается на основании того, вызвана ли она для l- или r-значения. Таким образом, первая версия функции add_item() вызывает константную версию l-значения функции clone(), а вторая версия вызывает версию ссылки на r-значение. Обратите внимание, что хотя в версии r-значения типом параметра sale является ссылка на r-значение, сам параметр sale (как и любая другая переменная) является l-значением (см. раздел 13.6.1). Поэтому для привязки ссылки на r-значение к параметру sale вызывается функция move().
Наша функция clone() является также виртуальной. Будет ли выполнена функция из класса Quote или Bulk_quote, зависит (как обычно) от динамического типа параметра sale. Независимо от того, копируются или перемещаются данные, функция clone() возвращает указатель на вновь зарезервированный объект его собственного типа. С этим объектом связывается указатель shared_ptr, и вызывается функция insert() для добавления этого вновь зарезервированного объекта к items. Обратите внимание: так как указатель shared_ptr поддерживает преобразование производного класса в базовый (см. раздел 15.2.2), указатель shared_ptr<Quote> можно привязать к Bulk_quote*.
Упражнения раздела 15.8.1
Упражнение 15.30. Напишите собственную версию класса Basket и используйте ее для вычисления цены за те же транзакции, что и в предыдущих упражнениях.