Язык программирования C++.
15.8. Контейнеры и наследование.
При использовании контейнера для хранения объектов из иерархии наследования их обычно хранят косвенно. Нельзя поместить объекты связанных наследованием типов непосредственно в контейнер, поскольку нет никакого способа определить контейнер, содержащий элементы разных типов.
В качестве примера определим вектор, содержащий несколько объектов для книг, которые клиент желает купать. Вполне очевидно, что не получится использовать вектор, содержащий объекты класса Bulk_quote. Нельзя преобразовать объекты класса Quote в объекты класса Bulk_quote (см. раздел 15.2.3), поэтому объекты класса Quote в этот вектор поместить не получится.
Может быть и не так очевидно, но вектор объектов типа Quote также нельзя использовать. В данном случае можно поместить объекты класса Bulk_quote в контейнер, но эти объекты перестанут быть объектами класса Bulk_quote:
vector<Quote> basket;
basket.push_back(Quote("0-2 01-82 4 7 0-1", 50));
// ok, но в basket копируется только часть Quote объекта
basket.push_back(Bulk_quote("0-201-54848-8", 50, 10, .25));
// вызов версии, определенной в Quote, выводит 750, т.е. 15 * $50
cout << basket.back().net_price(15) << endl;
Элементами вектора basket являются объекты класса Quote. Когда в вектор добавляется объект класса Bulk_quote, его производная часть игнорируется (см. раздел 15.2.3).
Поскольку при присвоении объекту базового класса объект производного класса усекается, контейнеры не очень удобны для хранения объектов разных классов, связанных наследственными отношениями.
Помещайте в контейнеры указатели (интеллектуальные), а не объекты.
Когда необходим контейнер, содержащий объекты, связанные наследованием, как правило, определяют контейнер указателей (предпочтительно интеллектуальных (см. раздел 12.1)) на базовый класс. Как обычно, динамический тип объекта, на который указывает этот указатель, мог бы быть типом базового класса или типом, производным от него:
vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("0-201-82470-1", 50));
basket.push_back(
make_shared<Bulk_quote>("0-201-54848-8", 50, 10, .25));
// вызов версии, определенной в Quote, выводит 562.5,
// т.е. со скидкой, меньше, чем 15 * $50
cout << basket.back()->net_price(15) << endl;
Поскольку вектор basket содержит указатели shared_ptr, для получения объекта, функция net_price() которого выполнится, следует обратиться к значению, возвращенному функцией basket.back(). Для этого в вызове функции net_price() используется оператор ->. Как обычно, вызываемая версия функции net_price() зависит от динамического типа объекта, на который указывает этот указатель.
Следует заметить, что вектор basket был определен как shared_ptr<Quote>, все же во втором вызове функции push_back() был передан указатель на объект класса Bulk_quote. Подобно тому, как можно преобразовать обычный указатель на производный тип в указатель на тип базового класса (см. раздел 15.2.2), можно также преобразовать интеллектуальный указатель на производный тип в интеллектуальный указатель на тип базового класса. Таким образом, вызов функции make_shared<Bulk_quote>() возвращает объект shared_ptr<Bulk_quote>, в который преобразуется shared_ptr<Quote> при вызове функции push_back(). В результате, несмотря на внешний вид, у всех элементов вектора basket будет тот же тип.
Упражнения раздела 15.8.
Упражнение 15.28. Определите вектор для содержания объектов класса Quote, но поместите в него объекты класса Bulk_quote. Вычислите общую сумму результатов вызова функции net_price() для всех элементов вектора.
Упражнение 15.29. Повторите предыдущую программу, но на сей раз храните указатели shared_ptr на объекты типа Quote. Объясните различие в сумме данной версии программы и предыдущей. Если никакой разницы нет, объясните почему.
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 и используйте ее для вычисления цены за те же транзакции, что и в предыдущих упражнениях.
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.
- 254.
- 255.
- 256.
- 257.
- 258.
- 259.
- 260.
- 261.
- 262.
- 263.
- 264.
- 265.
- 266.
- 267.
- 268.
- 269.
- 270.
- 271.
- 272.
- 273.
- 274.
- 275.
- 276.
- 277.
- 278.
- 279.
- 280.
- 281.
- 282.
- 283.
- 284.
- 285.
- 286.
- 287.
- 288.
- 289.
- 290.
- 291.
- 292.
- 293.
- 294.
- 295.
- 296.
- 297.
- 298.
- 299.
- 300.
- 301.
- 302.
- 303.
- 304.
- 305.
- 306.
- 307.
- 308.
- 309.
- 310.
- 311.
- 312.
- 313.
- 314.
- 315.
- 316.
- 317.
- 318.
- 319.
- 320.
- 321.
- 322.
- 323.
- 324.
- 325.
- 326.
- 327.
- 328.
- 329.
- 330.
- 331.
- 332.
- 333.
- 334.
- 335.
- 336.
- 337.
- 338.
- 339.
- 340.
- 341.
- 342.
- 343.
- 344.
- 345.
- 346.
- 347.
- 348.
- 349.
- 350.
- 351.
- 352.
- 353.
- 354.
- 355.
- 356.
- 357.
- 358.
- 359.
- 360.
- 361.
- 362.
- 363.
- 364.
- 365.
- 366.
- 367.
- 368.
- 369.
- 370.
- 371.
- 372.
- 373.
- 374.
- 375.
- 376.
- 377.
- 378.
- 379.
- 380.
- 381.
- 382.
- 383.
- 384.
- 385.
- 386.
- 387.
- 388.
- 389.
- 390.
- 391.
- 392.
- 393.
- 394.
- 395.
- 396.
- 397.
- 398.
- 399.
- 400.
- 401.
- 402.
- 403.
- 404.
- 405.
- 406.
- 407.
- 408.
- 409.
- 410.
- 411.
- 412.
- 413.
- 414.
- 415.
- 416.
- 417.
- 418.
- 419.
- 420.
- 421.
- 422.
- 423.
- 424.
- 425.
- 426.
- 427.
- 428.
- 429.
- 430.
- 431.
- 432.