Язык программирования C Пятое издание
Скачать 1.85 Mb.
|
Page 780/1103 15.7.2. Синтезируемые функции управления копированием и наследование Синтезируемые функции-члены управления копированием в базовом или производном классе выполняются, как любой другой синтезируемый конструктор, оператор присвоения или деструктор: они почленно инициализируют, присваивают или удаляют члены самого класса. Кроме того, эти синтезируемые члены инициализируют, присваивают или удаляют прямую базовую часть объекта при помощи соответствующей функции базового класса. Соответствующие примеры приведены ниже. • Синтезируемый стандартный конструктор класса Bulk_quote запускает стандартный конструктор класса Disc_quote, который в свою очередь запускает стандартный конструктор класса Quote. • Стандартный конструктор класса Quote инициализирует по умолчанию переменную-член bookNo пустой строкой и использует внутриклассовый инициализатор для инициализации переменной-члена price нулем. • Когда конструктор класса Quote завершает работу, конструктор класса Disc_quote продолжает ее, используя внутриклассовые инициализаторы для инициализации переменных qty и discount. • Когда завершает работу конструктор класса Disc_quote, конструктор класса Bulk_quote продолжает ее, но не выполняет никаких других действий. Точно так же синтезируемый конструктор копий класса Bulk_quote использует (синтезируемый) конструктор копий класса Disc_quote, который использует (синтезируемый) конструктор копий класса Quote. Конструктор копий класса Quote копирует переменные-члены bookNo и price; а конструктор копий класса Disc_quote копирует переменные-члены qty и discount. Следует заметить, что не имеет значения, синтезируется ли функция-член базового класса (как в случае иерархии Quote) или имеет предоставленное пользователем определение. Важно лишь то, что соответствующая функция-член доступна (см. раздел 15.5) и что она не удаленная. Каждый из классов иерархии Quote использует синтезируемый деструктор. Производные классы делают это неявно, тогда как класс Quote делает это явно, определяя свой (виртуальный) деструктор как = default. Синтезируемый деструктор (как обычно) пуст, и его неявная часть удаляет члены класса (см. раздел 13.1.3). В дополнение к удалению собственных членов фаза удаления деструктора в производном классе удаляет также свою прямую базовую часть. Этот деструктор в свою очередь вызывает деструктор своего прямого базового класса, если он есть. И так далее до корневого класса иерархии. Как уже упоминалось, у класса Quote нет синтезируемых функций перемещения, поскольку он определяет деструктор. При каждом перемещении объекта Quote (см. раздел 13.6.2) будут использоваться (синтезируемые) функции копирования. Как будет продемонстрировано ниже, тот факт, что у класса Quote нет функций перемещения, означает, что его производные классы также не будут их иметь. Базовые классы и удаленные функции управления копированием в производном классе Синтезируемый стандартный конструктор или любая из функций-членов управления копированием базового либо производного класса может быть определена как удаленная по тем же причинам, что и в любом другом классе (см. раздел 13.1.6 и раздел 13.6.2). Кроме того, способ определения базового класса может вынудить член производного класса стать Page 781/1103 удаленным. • Если стандартный конструктор, конструктор копий, оператор присвоения копии или деструктор в базовом классе удалены или недоступны (раздел 15.5), то соответствующая функция-член в производном классе определяется как удаленная, поскольку компилятор не может использовать функцию-член базового класса для создания, присвоения или удаления части объекта базового класса. • Если у базового класса недоступен или удален деструктор, то синтезируемые стандартный конструктор и конструктор копий в производных классах определяются как удаленные, поскольку нет никакого способа удалить базовую часть производного объекта. • Как обычно, компилятор не будет синтезировать удаленную функцию перемещения. Если использовать синтаксис = default для создания функции перемещения, то это будет удаленная функция в производном классе, если соответствующая функция в базовом классе будет удалена или недоступна, поскольку часть базового класса не может быть перемещена. Конструктор перемещения также будет удален, если деструктор базового класса окажется удален или недоступен. Для примера рассмотрим базовый класс В: class B { public: B(); B(const B&) = delete; // другие члены, исключая конструктор перемещения }; class D : public B { // нет конструкторов }; D d; // ok: синтезируемый стандартный конструктор класса D использует // стандартный конструктор класса В D d2(d); // ошибка: синтезируемый конструктор копий класса D удален D d3(std::move(d)); // ошибка: неявно использованный удаленный Page 782/1103 // конструктор копий класса D Класс имеет доступный стандартный конструктор и явно удаленный конструктор копий. Поскольку конструктор копий определяется, компилятор не будет синтезировать для класса В конструктор перемещения (см. раздел 13.6.2). В результате невозможно ни переместить, ни скопировать объекты типа В. Если бы класс, производный от типа В, хотел позволить своим объектам копирование или перемещение, то этот производный класс должен был бы определить свои собственные версии этих конструкторов. Конечно, этот класс должен был бы решить, как скопировать или переместить члены в эту часть базового класса. Практически, если у базового класса нет стандартного конструктора копий или конструктора перемещения, то его производные классы также обычно не будут их иметь. Функции перемещения и наследование Как уже упоминалось, большинство базовых классов определяет виртуальный деструктор. В результате по умолчанию базовые классы вообще не получают синтезируемых функций перемещения. Кроме того, по умолчанию классы, производные от базового класса, у которого нет функций перемещения, также не получают синтезируемых функций перемещения. Поскольку отсутствие функции перемещения в базовом классе подавляет синтез функций перемещения в его производных классах, базовые классы обычно должны определять функции перемещения, если это имеет смысл. Наш класс Quote может использовать синтезируемые версии. Однако класс Quote должен определить эти члены явно. Как только он определит собственные функции перемещения, он должен будет также явно определить версии копирования (см. раздел 13.6.2): class Quote { public: Quote() = default; // почленная инициализация по умолчанию Quote(const Quote&) = default; // почленное копирование Quote(Quote&&) = default; // почленное копирование Quote& operator=(const Quote&) = default; // присвоение копии Quote& operator=(Quotes&) = default; // перемещение virtual Quote() = default; // другие члены, как прежде }; Page 783/1103 Теперь объекты класса Quote будут почленно копироваться, перемещаться, присваиваться и удаляться. Кроме того, классы, производные от класса Quote, также автоматически получат синтезируемые функции перемещения, если у них не будет членов, которые воспрепятствуют перемещению. Упражнения раздела 15.7.2 Упражнение 15.25. Зачем определять стандартный конструктор для класса Disc_quote? Как повлияет на поведение класса Bulk_quote, если вообще повлияет, удаление этого конструктора? 15.7.3. Функции-члены управления копированием производного класса Как упоминалось в разделе 15.2.2, фаза инициализации конструктора производного класса инициализирует часть (части) базового класса производного объекта наряду с инициализацией его собственных членов. В результате конструкторы копирования и перемещения для производного класса должны копировать и перемещать члены своей базовой части наравне с производной. Точно так же оператор присвоения производного класса должен присваивать члены базовой части производного объекта. В отличие от конструкторов и операторов присвоения, деструктор несет ответственность только за освобождение ресурсов, зарезервированных производным классом. Помните, что члены объекта освобождаются неявно (см. раздел 13.1.3). Точно так же часть базового класса объекта производного класса освобождается автоматически. Когда производный класс определяет функцию копирования или перемещения, эта функция несет ответственность за копирование или перемещение всего объекта, включая члены базового класса. Определение конструктора копии или перемещения производного класса При определении конструктора копии или перемещения (см. раздел 13.1.1 и раздел 13.6.2) для производного класса обычно используется соответствующий конструктор базового класса, инициализирующий базовую часть объекта: class Base { /* ... */ }; class D: public Base { public: // по умолчанию стандартный конструктор базового класса // инициализирует базовую часть объекта // чтобы использовать конструктор копии или перемещения, его следует // вызвать явно // Page 784/1103 конструктор в списке инициализации конструктора D(const D& d) : Base(d) // копирование базовых членов /* инициализаторы для членов класса D */ { /* ... */ } D(D&& d): Base(std::move(d)) // перемещение базовых членов /* инициализаторы для членов класса D */ { /* ... */ } }; Инициализатор Base(d) передает объект класса D конструктору базового класса. Хотя в принципе у класса Base может быть конструктор с параметром типа D, на практике это очень маловероятно. Вместо этого инициализатор Base(d) будет (обычно) соответствовать конструктору копий класса Base. В этом конструкторе объект d будет связан с параметром типа Base&. Конструктор копий класса Base скопирует базовую часть объекта d в создаваемый объект. Будь инициализатор для базового класса пропущен, для инициализации базовой части объекта класса D будет использован стандартный конструктор класса Base. // вероятно, неправильное определение конструктора копий D // часть базового класса инициализируется по умолчанию, а не копией D(const D& d) /* инициализаторы членов класса, но не базового класса */ { /* ... */ } Предположим, что конструктор класса D копирует производные члены объекта d. Этот вновь созданный объект был бы настроен странно: его члены класса Base содержали бы значения по умолчанию, в то время как его члены класса D были бы копиями данных из другого объекта. По умолчанию стандартный конструктор базового класса инициализирует часть базового класса объекта производного. Если необходимо копирование (или перемещение) части базового класса, следует явно использовать конструктор копий (или перемещения) для базового класса в списке инициализации конструктора производного. Оператор присвоения производного класса Подобно конструктору копирования и перемещения, оператор присвоения производного класса (см. раздел 13.1.2 и раздел 13.6.2) должен присваивать свою базовую часть явно: // Page 785/1103 Base::operator=(const Base&) не вызывается автоматически D &D::operator=(const D &rhs) { Base::operator=(rhs); // присваивает базовую часть // присвоение членов в производном классе, как обычно, // отработка самоприсвоения и освобождения ресурсов return *this; } Этот оператор начинается с явного вызова оператора присвоения базового класса, чтобы присвоить члены базовой части объекта производного. Оператор базового класса (по-видимому, правильно) отработает случай присвоения себя себе и, если нужно, освободит прежнее значение в базовой части левого операнда и присвоит новое значение правой. По завершении работы оператора продолжается выполнение всего необходимого для присвоения членов в производном классе. Следует заметить, что конструктор или оператор присвоения производного класса может использовать соответствующую функцию базового класса независимо от того, определил ли базовый класс собственную версию этого оператора или использует синтезируемую. Например, вызов оператора Base::operator= выполняет оператор присвоения копии в классе Base. При этом несущественно, определяется ли этот оператор классом Base явно или синтезируется компилятором. Деструктор производного класса Помните, переменные-члены объекта неявно удаляются после завершения выполнения тела деструктора (см. раздел 13.1.3). Точно так же части базового класса объекта тоже удаляются неявно. В результате, в отличие от конструкторов и операторов присвоения, производный деструктор отвечает за освобождение только тех ресурсов, которые зарезервировал производный класс: class D: public Base { public: // Base::Base вызывается автоматически D() { /* освободить члены производного класса */ } }; Объекты удаляются в порядке, противоположном их созданию: сначала выполняется деструктор производного класса, а затем деструкторы базового класса, назад по иерархии наследования. Вызовы виртуальных функций в конструкторах и деструкторах Page 786/1103 Как уже упоминалось, сначала создается часть базового класса в объекте производного. Пока выполняется конструктор базового класса, производная часть объекта остается неинициализированной. Точно так же производные объекты удаляются в обратном порядке, чтобы при выполнении деструктора базового класса производная часть уже была удалена. В результате на момент выполнения членов базового класса объект оказывается в незавершенном состоянии. Чтобы приспособиться к этой незавершенности, компилятор рассматривает объект как изменяющий свой тип во время создания или удаления. Таким образом, во время создания объекта он считается объектом того же класса, что и конструктор; вызовы виртуальной функции будут связаны так, как будто у объекта тот же тип, что и у самого конструктора. Аналогично для деструктора. Эта привязка относится к виртуальным функциям, вызванным непосредственно или косвенно, из функции, которую вызывает конструктор (или деструктор). Чтобы понять это поведение, рассмотрим, что произошло бы, если бы версия виртуальной функции производного класса была вызвана из конструктора базового класса. Эта виртуальная функция, вероятно, обратится к членам производного объекта. В конце концов, если бы виртуальная функция не должна была использовать члены производного объекта, то производный класс, вероятно, мог бы использовать ее версию в базовом классе. Но во время выполнения конструктора базового класса эти члены остаются неинициализированными. Если бы такой доступ был разрешен, то работа программы, вероятно, закончилась бы катастрофически. Если конструктор или деструктор вызывает виртуальную функцию, то выполняемая версия будет соответствовать типу самого конструктора или деструктора. Упражнения раздела 15.7.3 Упражнение 15.26. Определите для классов Quote и Bulk_quote функции-члены управления копированием, осуществляющие те же действия, что и синтезируемые версии. Снабдите их и другие конструкторы операторами вывода, идентифицирующими выполняемую функцию. Напишите программу с использованием этих классов и укажите, какие объекты будут созданы и удалены. Сравните свои предположения с выводом и продолжите экспериментировать, пока ваши предположения не станут правильными. 15.7.4. Унаследованные конструкторы По новому стандарту производный класс может многократно использовать конструкторы, определенные его прямым базовым классом. Хотя, как будет продемонстрировано далее, такие конструкторы не наследуются в обычном смысле этого слова, о них, тем не менее, говорят как об унаследованных. По тем же причинам, по которым класс может инициализировать только свой прямой базовый класс, класс может наследовать конструкторы только от своего прямого базового класса. Класс не может унаследовать стандартный конструктор, конструктор копий и перемещения. Если производный класс не определяет эти конструкторы сам, то компилятор синтезирует их, как обычно. Производный класс наследует конструкторы своего базового класса при помощи объявления using, в котором указан его (прямой) базовый класс. В качестве примера можно переопределить класс Bulk_quote (см. раздел 15.4) так, чтобы он унаследовал конструкторы от класса Disc_quote: class Bulk_quote : public Disc_quote { Page 787/1103 public: using Disc_quote::Disc_quote; // наследует конструкторы Disc_quote double net_price(std::size_t) const; }; Обычно объявление using просто делает имя видимым в текущей области видимости. Применительно к конструктору объявление using приводит к созданию компилятором кода. Компилятор создает в производном классе конструктор, соответствующий каждому конструктору в базовом классе. Таким образом, для каждого конструктора в базовом классе компилятор создает в производном классе конструктор с таким же списком параметров. Эти созданные компилятором конструкторы имеют такую форму: производный ( параметры ) : базовый ( аргументы ) { } где производный — имя производного класса; базовый — имя базового класса; параметры — список параметров конструктора; аргументы передают параметры из конструктора производного класса в конструктор базового. В классе Bulk_quote унаследованный конструктор был бы эквивалентен следующему: Bulk_quote(const std::string& book, double price, std::size_t qty, double disc): Disc_quote(book, price, qty, disc) { } Если у производного класса есть какие-нибудь собственные переменные-члены, они инициализируются по умолчанию (см. раздел 7.1.4). Характеристики унаследованного конструктора В отличие от объявлений using для обычных членов, объявление using для конструктора не изменяет уровень доступа унаследованного конструктора (конструкторов). Например, независимо от того, где расположено объявление using, закрытый конструктор в базовом классе остается закрытым в производном; то же относится к защищенным и открытым конструкторам. Кроме того, объявление using не может использовать определение как explicit или constexpr. Если конструктор объявлен как explicit (см. раздел 7.5.4) или constexpr (см. раздел 7.5.6) в базовом классе, у унаследованного конструктора будет то же свойство. Если у конструктора базового класса есть аргументы по умолчанию (см. раздел 6.5.1), они не Page 788/1103 наследуются. Вместо этого производный класс получает несколько унаследованных конструкторов, в которых каждый параметр с аргументом по умолчанию благополучно пропущен. Например, если у базового класса будет конструктор с двумя параметрами, у второго из которых будет аргумент по умолчанию, то производный класс получит два конструктора: один с обоими параметрами (и никакого аргумента по умолчанию) и второй конструктор с одним параметром, соответствующим левому параметру без аргумента по умолчанию в базовом классе. Если у базового класса есть несколько конструкторов, то за двумя исключениями производный класс унаследует каждый из конструкторов своего базового класса. Первое исключение — производный класс может унаследовать некоторые конструкторы и определить собственные версии других конструкторов. Если производный класс определяет конструктор с теми же параметрами, что и конструктор в базовом классе, то этот конструктор не наследуется. Конструктор, определенный в производном классе, используется вместо унаследованного конструктора. Второе исключение — стандартный конструктор, конструктор копий и конструктор перемещения не наследуются. Эти конструкторы синтезируются с использованием обычных правил. Унаследованный конструктор не рассматривается как пользовательский конструктор. Поэтому у класса, который содержит только унаследованные конструкторы, будет синтезируемый стандартный конструктор. Упражнения раздела 15.7.4 Упражнение 15.27. Переопределите свой класс Bulk_quote так, чтобы унаследовать его конструкторы. 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)); // Page 789/1103 вызов версии, определенной в 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. Объясните различие в сумме данной версии программы и Page 790/1103 предыдущей. Если никакой разницы нет, объясните почему. 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 содержит несколько стратегий расценок, Page 791/1103 // упорядоченных по сравниваемому элементу 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 // Page 792/1103 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 (или класса производного от него). Для выяснения Page 793/1103 количества элементов в контейнере 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 Page 794/1103 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())); } // другие члены, как прежде }; Page 795/1103 Как и сама функция 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 и используйте ее для вычисления цены за те же транзакции, что и в предыдущих упражнениях. 15.9. Возвращаясь к запросам текста В качестве последнего примера наследования дополним приложение текстового запроса из раздела 12.3. Написанные в этом разделе классы позволят искать вхождения данного слова в файле. Дополним эту систему возможностью создавать более сложные запросы. В этих примерах запросы будут выполняться к тексту следующей истории: Alice Emma has long flowing red hair. Her Daddy says when the wind blows through her hair, it looks almost alive, like a fiery bird in flight. A beautiful fiery bird, he tells her, magical but untamed. "Daddy, shush, there is no such thing," she tells him, at the same time wanting him to tell her more. Shyly, she asks, "I mean, Daddy, is there?" Система должна поддерживать следующие запросы. • Запросы слов находят все строки, соответствующие заданной строке: Executing Query for: Daddy Daddy occurs 3 times Page 796/1103 (line 2) Her Daddy says when the wind blows (line 7) "Daddy, shush, there is no such thing," (line 10) Shyly, she asks, "I mean, Daddy, is there?" • Инверсный запрос с использованием оператора возвращает строки, которые не содержат заданную строку: Executing Query for: (Alice) (Alice) occurs 9 times (line 2) Her Daddy says when the wind blows (line 3) through her hair, it looks almost alive, (line 4) like a fiery bird in flight. • Запросы ИЛИ с использованием оператора | возвращают строки, содержащие любую из двух заданных строк: Executing Query for: (hair | Alice) (hair | Alice) occurs 2 times (line 1) Alice Emma has long flowing red hair, (line 3) through her hair, it looks almost alive, • Запросы И с использованием оператора & возвращают строки, содержащие обе заданные строки: Executing query for: (hair & Alice) (hair & Alice) occurs 1 time (line 1) Alice Emma has long flowing red hair. Кроме того, нужна возможность объединить эти операторы так fiery & bird | wind Для обработки составных выражений, таких как в этом примере, будут использованы обычные правила приоритета С++ (см. раздел 4.1.2). Таким образом, этому запросу соответствует строка, в которой присутствуют слова fiery и bird или слово wind: Executing Query for: ((fiery & bird) | wind) ((fiery & bird) | wind) occurs 3 times (line 2) Her Daddy says when the wind blows (line 4) like a fiery bird in flight. (line 5) A beautiful fiery bird, he tells her, Page 797/1103 В отображаемом результате для указания способа интерпретации запроса используются круглые скобки. Подобно первоначальной реализации, система не должна отображать одинаковые строки несколько раз. 15.9.1. Объектно-ориентированное решение Для представления запросов на поиск слов вполне логично было бы использовать класс TextQuery (см. раздел 12.3.2), а другие классы запросов можно было бы получить как производные от этого класса. Однако такой подход неверен. Концептуально инверсный запрос не является разновидностью запроса на поиск слова. Инверсный запрос — это скорее запрос типа "имеет" (запрос на поиск слова или любой другой тип запроса), результат которого интерпретируется негативно. Исходя из этого можно сделать вывод, что разные виды запросов следует оформить как независимые классы, которые совместно используют общий базовый класс: WordQuery // Daddy NotQuery // Alice OrQuery // hair | Alice AndQuery // hair & Alice Эти классы будет иметь только две функции. • Функция eval(), получающая объект класса TextQuery и возвращающая объект класса QueryResult. Для поиска запрошенной строки функция eval() будет использовать переданный объект класса TextQuery. • Функция rep(), возвращающая строковое представление базового запроса. Эту функцию использует функция eval() для создания объекта класса QueryResult, представляющего соответствия, а также оператор вывода, отображающий выражение запроса. Абстрактный базовый класс Как уже упоминалось, все четыре типа запроса не связаны друг с другом наследованием; концептуально они элементы одного уровня. Каждый класс использует тот же интерфейс, а значит, для представления этого интерфейса следует определить абстрактный базовый класс (см. раздел 15.4). Назовем этот абстрактный базовый класс Query_base, поскольку он должен служить корневым классом иерархии запроса. Ключевая концепция. Наследование или композиция Проектирование иерархии наследования — это достаточно сложная тема, которая выходит за рамки данного вводного курса. Однако имеет смысл упомянуть об одном достаточно важном факторе проектирования, с которым должен быть знаком каждый программист. Page 798/1103 При определении класса как открыто производного от другого производный и базовый классы реализуют взаимоотношения типа " является " (is а). В хорошо проработанных иерархиях объекты открыто унаследованных классов применимы везде, где ожидается объект базового класса. Еще одним популярным способом взаимоотношений классов является принцип " имеет " (has а). Типы, связанные отношениями " имеет ", подразумевают принадлежность. В рассматриваемом примере с книжным магазином базовый класс представляет концепцию книги, продаваемой по предусмотренной цене, а класс Bulk_quote " является " конкретной книгой, продаваемой по розничной цене с определенной стратегией скидок. Классы приложения книжного магазина " имеют " цену и ISBN. Класс Query_base определит функции eval() и rep() как чистые виртуальные (см. раздел 15.4). Каждый из классов, представляющих специфический вид запроса, должен переопределить эти функции. Классы WordQuery и NotQuery унаследуем непосредственно от класса Query_base. У классов AndQuery и OrQuery будет одна общая особенность, которой не будет у остальных классов в системе: у каждого будет по два операнда. Для моделирования этой особенности определим другой абстрактный базовый класс, BinaryQuery, представляющий запросы с двумя операндами. Классы AndQuery и OrQuery наследуются от класса BinaryQuery, который в свою очередь наследуется от класса Query_base. Результатом этих решений будет проект классов, представленный на рис. 15.2. Рис. 15.2. Иерархия наследования Query_base Сокрытие иерархии в классе интерфейса Рассматриваемая программа будет отрабатывать запросы, а не создавать их. Но чтобы запустить программу на выполнение, необходимо определить способ создания запроса. Проще всего сделать это непосредственно в коде при помощи выражения С++. Например, чтобы создать описанный ранее составной запрос, можно использовать следующий код: Query q = Query("fiery") & Query("bird") | Query ("wind"); Это довольно сложное описание неявно предполагает, что код пользовательского уровня не будет использовать унаследованные классы непосредственно. Вместо этого будет создан класс интерфейса по имени Query (Запрос), который и скроет иерархию. Класс Query будет хранить указатель на класс Query_base. Этот указатель будет связан с объектом типа, производного от класса Query_base. Класс Query будет предоставлять те же функции, что и классы Query_base: функцию eval() для обработки соответствующего запроса и функцию rep() для создания строковой версии запроса. В нем также будет определен перегруженный оператор вывода, чтобы отображать соответствующий запрос. Пользователи будут создавать объекты класса Query_base и работать с ними только косвенно, через функции объектов класса Query. Для класса Query, наряду с получающим строку конструктором, определим три перегруженных оператора. Каждая из этих функций будет динамически резервировать новый объект типа, производного от класса Query_base: • Оператор & создает объект класса Query, связанный с новым объектом класса AndQuery. • Оператор | создает объект класса Query, связанный с новым объектом класса OrQuery. Page 799/1103 • Оператор создает объект класса Query, связанный с новым объектом класса NotQuery. • Конструктор класса Query, получающий строку и создающий новый объект класса WordQuery. Как работают эти классы Следует понять, что работа этого приложения состоит в основном из построения объектов для представления запросов пользователя. Например, приведенное выше выражение создает коллекцию взаимодействовавших объектов, представленных на рис. 15.3. Рис. 15.3. Объекты, созданные выражениями запросов Как только создано дерево объектов, обработка (или отображение) данного запроса сводится к простому процессу (осуществляемому компилятором), который, следуя по линиям, опрашивает каждый объект дерева, чтобы выполнить (или отобразить) необходимые действия. Например, если происходит вызов функции eval() объекта q (т.е. корневого класса дерева), функция eval() опросит объект класса OrQuery, на который он указывает. Обработка этого объекта класса OrQuery приведет к вызову функции eval() для двух его операндов, что, в свою очередь, приведет к вызову функции eval() для объектов классов AndQuery и WordQuery, которые осуществляют поиск слова wind. Обработка объекта класса AndQuery, в свою очередь, приведет к обработке двух его объектов класса WordQuery, создав результаты для слов fiery и bird соответственно. Новичкам в объектно-ориентированном программировании зачастую трудней всего разобраться в проекте программы. Но как только проект станет абсолютно понятен, его реализация не вызывает проблем. Чтобы проще было понять суть проекта, все используемые в этом примере классы были обобщены в табл. 15.1. Таблица 15.1. Обзор проекта программы Классы и операторы интерфейса программы запросов TextQuery Класс, который читает указанный файл и создает карту поиска. Этот класс предоставляет функцию поиска query(), которая получает строковый аргумент и возвращает объект класса QueryResult, представляющий строки, в которых присутствует ее аргумент (см. раздел 12.3.2) QueryResult Класс, содержащий результаты вызова функции query() (см. раздел 12.3.2) Query Класс интерфейса, указывающий на объект типа, производного от класса Query_base Query q(s) Связывает объект q класса Query с новым объектом класса WordQuery, содержащим строку s q1 & q2 Возвращает объект класса Query, связанный с новым объектом класса AndQuery, который содержит объекты q1 и q2 q1 | q2 Возвращает объект класса Query, связанный с новым объектом класса OrQuery, содержащим объекты q1 и q2 q Возвращает объект класса Query, связанный с новым объектом класса NotQuery, содержащим объект q Классы реализации программы запросов Query_base Абстрактный класс, базовый для классов запроса WordQuery Класс, производный от класса Query_base, который ищет указанное слово NotQuery Класс, производный от класса Query_base, представляющий набор строк, в которых указанный операнд Query отсутствует BinaryQuery Абстрактный базовый класс, производный от класса Query_base, который представляет запросы с двумя операндами типа Query OrQuery Класс, производный от класса BinaryQuery, который возвращает набор номеров строк, в которых присутствует хотя бы один из операндов AndQuery Класс, производный от класса BinaryQuery, который возвращает набор номеров строк, в которых присутствуют оба операнда Упражнения раздела 15.9.1 Упражнение 15.31. При условии, что s1, s2, s3 и s4 являются строками укажите, какие объекты создаются в следующих выражениях: (a) Query(s1) | Query(s2) & Query(s3); (b) Query(s1) | (Query(s2) & Query(s3)); Page 800/1103 (c) (Query(s1) & (Query(s2)) | (Query(s3) & Query(s4))); 15.9.2. Классы Query_base и Query Начнем реализацию с определения класса Query_base: // абстрактный класс, являющийся базовым для конкретных типов запроса; // все члены закрыты class Query_base { friend class Query; protected: using line_no = TextQuery::line_no; // используется в функциях eval() virtual Query_base() = default; private: // eval() возвращает соответствующий запросу QueryResult virtual QueryResult eval(const TextQuery&) const = 0; // rep() строковое представление запроса virtual std::string rep() const = 0; }; Обе функции, eval() и rep(), являются чистыми виртуальными, что делает класс Query_base абстрактным базовым (см. раздел 15.4). Поскольку класс Query_base не предназначен для пользователей и непосредственного использования в производных классах, у него нет открытых членов. Класс Query_base будет использоваться только через объекты класса Query. Класс предоставляет дружественные отношения классу Query, поскольку его члены вызывают виртуальные функции класса Query_base. Защищенный член line_no будет использоваться в функциях eval(). Деструктор также будет защищен, поскольку он используется (неявно) деструкторами в производных классах. Класс Query Класс Query предоставляет интерфейс к иерархии наследования Query_base и скрывает ее. Каждый объект класса Query содержит указатель shared_ptr на соответствующий объект Page 801/1103 класса Query_base. Поскольку класс Query — единственный интерфейс к классам иерархии Query_base, он должен определить собственные версии функций eval() и rep(). Конструктор Query(), получающий строку, создаст новый объект класса WordQuery и свяжет его указатель-член shared_ptr с этим недавно созданным объектом. Операторы &, | и создают объекты AndQuery, OrQuery и NotQuery соответственно. Эти операторы возвращают объект класса Query, связанный с созданным им объектом. Для поддержки этих операторов класс Query нуждается в конструкторе, получающем указатель shared_ptr на класс Query_base и сохраняющем его. Сделаем этот конструктор закрытым, поскольку объекты класса Query_base не предназначены для определения общим пользовательским кодом. Так как этот конструктор является закрытым, операторы следует сделать дружественными. Исходя из приведенного выше проекта, сам класс Query довольно прост: // класс интерфейса для взаимодействия с иерархией // наследования Query_base class Query { // эти операторы должны обращаться к указателю shared_ptr friend Query operator(const Query &); friend Query operator|(const Query&, const Query&); friend Query operator&(const Query&, const Query&); public: Query(const std::string&); // создает новый WordQuery // функции интерфейса: вызывают соответствующий оператор Query_base QueryResult eval(const TextQuery &t) const { return q->eval(t); } std::string rep() const { return q->rep(); } private: Query(std::shared_ptr<Query_base> query): q(query) { } std::shared_ptr<Query_base> q; }; Начнем с объявления дружественных операторов, создающих объекты класса Query. Эти Page 802/1103 операторы должны быть друзьями, чтобы использовать закрытый конструктор. В открытом интерфейсе для класса Query объявляется, но еще не может быть определен получающий строку конструктор. Этот конструктор создает объект класса WordQuery, поэтому невозможно определить этот конструктор, пока не определен сам класс WordQuery. Два других открытых члена представляют интерфейс для класса Query_base. В каждом случае оператор класса Query использует свой указатель класса Query_base для вызова соответствующей (виртуальный) функции класса Query_base. Фактически вызываемая версия определяется во время выполнения и будет зависеть от типа объекта, на который указывает указатель q. Оператор вывода класса Query Оператор вывода — хороший пример того, как работает вся система запросов: std::ostream & operator<<(std::ostream &os, const Query &query) { // Query::rep() осуществляет виртуальный вызов через свой // указатель Query_base на rep() return os << query.rep(); } При выводе объекта класса Query оператор вывода вызывает (открытую) функцию-член rep() класса Query. Эта функция осуществляет виртуальный вызов через свой указатель-член функции-члена rep() объекта, на который указывает данный объект класса Query. Query andq = Query(sought1) & Query(sought2); cout << andq << endl; Таким образом, когда в коде встречается оператор вывода, он вызывает функцию Query::rep() объекта andq. Функция Query::rep() в свою очередь осуществляет виртуальный вызов через свой указатель класса Query_base на версию функции rep() класса Query_base. Поскольку объект andq указывает на объект класса AndQuery, этот вызов выполнит функцию AndQuery::rep(). Упражнения раздела 15.9.2 |