Виртуальный ввод/вывод
Первая виртуальная операция, которую мы хотели реализовать, – это печать запроса на стандартный вывод либо в файл:
ostream& print( ostream &os = cout ) const;
Функцию print()
следует объявить виртуальной, поскольку ее реализации зависят от типа, но нам нужно вызывать ее через указатель типа Query*. Например, для класса AndQuery эта функция могла бы выглядеть так:
ostream& AndQuery::print( ostream &os ) const { _lop->print( os ); os << " && "; _rop->print( os ); |
|
}
Необходимо объявить print()
виртуальной функцией в абстрактном базовом Query, иначе мы не сможем вызвать ее для членов классов AndQury, OrQuery и NotQuery, являющихся указателями на операнды соответствующих запросов типа Query*. Однако для самого Query
разумной реализации print() не существует. Поэтому мы определим ее как пустую функцию, а потом сделаем чисто виртуальной:
class Query { public: virtual ostream& print( ostream &os=cout ) const {} // ... |
};
В базовом классе, где виртуальная функция появляется в первый раз, ее объявлению должно предшествовать ключевое слово virtual. Если же ее определение находится вне этого класса, повторно употреблять virtual не следует. Так, данное определение print()
приведет к ошибке компиляции:
// ошибка: ключевое слово virtual может появляться // только в определении класса |
virtual ostream& Query::print( ostream& ) const { ... }
Правильный вариант не должен включать слово virtual.
Класс, в котором впервые появляется виртуальная функция, должен определить ее или объявить чисто виртуальной (напомним, что пока мы определили ее как пустую). В производном классе может быть либо определена собственная реализация той же функции, которая в таком случае становится активной для всех объектов этого класса, либо унаследована реализация из базового класса. Если в производном классе определена собственная реализация, то говорят, что она замещает реализацию из базового.
Прежде чем приступать к рассмотрению реализаций print() для наших четырех производных классов, обратим внимание на употребление скобок в запросе. Например, с помощью
fiery && bird || shyly
пользователь ищет вхождения пары слов
fiery bird
или одного слова
shyly
С другой стороны, запрос
fiery && ( bird || hair )
найдет все вхождения любой из пар
fiery bird
или
fiery hair
Если наши реализации print() не будут показывать скобки в исходном запросе, то для пользователя они окажутся почти бесполезными. Чтобы сохранить эту информацию, введем в наш абстрактный базовый класс Query два нестатических члена, а также функции доступа к ним (подобное расширение класса – естественная часть эволюции иерархии):
class Query { public: // ... // установить _lparen и _rparen void lparen( short lp ) { _lparen = lp; } void rparen( short rp ) { _rparen = rp; } // получить значения_lparen и _rparen short lparen() { return _lparen; } short rparen() { return _rparen; } // напечатать левую и правую скобки void print_lparen( short cnt, ostream& os ) const; void print_rparen( short cnt, ostream& os ) const; protected: // счетчики левых и правых скобок short _lparen; short _rparen; // ... |
_lparen – это количество левых, а _rparen – правых скобок, которое должно быть выведено при распечатке объекта. (В разделе 17.7 мы покажем, как вычисляются такие величины и как происходит присваивание обоим членам.) Вот пример обработки запроса с большим числом скобок:
==> ( untamed || ( fiery || ( shyly ) ) )
evaluate word: untamed
_lparen: 1
_rparen: 0
evaluate Or
_lparen: 0
_rparen: 0
evaluate word: fiery
_lparen: 1
_rparen: 0
evaluate 0r
_lparen: 0
_rparen: 0
evaluate word: shyly
_lparen: 1
_rparen: 0
evaluate right parens:
_rparen: 3
( untamed ( 1 ) lines match
( fiery ( 1 ) lines match
( shyly ( 1 ) lines match
( fiery || (shyly ( 2 ) lines match3
( untamed || ( fiery || ( shyly ))) ( 3 ) lines match
Requested query: ( untamed || ( fiery || ( shyly ) ) )
( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her,
( 4 ) magical but untamed. "Daddy, shush, there is no such thing,"
( 6 ) Shyly, she asks, "I mean, Daddy, is there?"
Реализация print() для класса NameQuery:
ostream& NameQuery:: print( ostream &os ) const { if ( _lparen ) print_lparen( _lparen, os ); os << _name; if ( _rparen ) print_rparen( _rparen, os ); return os; |
А так выглядит объявление:
class NameQuery : public Query { public: virtual ostream& print( ostream &os ) const; // ... |
Чтобы реализация виртуальной функции в производном классе замещала реализацию из базового, прототипы функций обязаны совпадать. Например, если бы мы опустили слово const или объявили еще один параметр, то реализация print() в NameQuery не заместила бы реализацию из базового класса. Возвращаемые значения также должны быть одинаковыми за одним исключением: значение, возвращенное реализацией в производном классе, может принадлежать к типу класса, который открыто наследует классу значения, возвращаемого реализацией в базовом классе. Если бы реализация из базового класса возвращала значение типа Query*, то реализация из производного могла бы возвращать NameQuery*. (Позже при работе с функцией clone() мы покажем, зачем это нужно.) Вот объявление и реализация print() в NotQuery:
class NotQuery : public Query { public: virtual ostream& print( ostream &os ) const; // ... |
ostream& NotQuery:: print( ostream &os ) const { os << " ! "; if ( _lparen ) print_lparen( _lparen, os ); _op->print( os ); if ( _rparen ) print_rparen( _rparen, os ); return os; |
}
Разумеется, вызов print()
через _op – виртуальный.
Объявления и реализации этой функции в классах AndQuery и OrQuery практически дублируют друг друга. Поэтому приведем их только для AndQuery:
class AndQuery : public Query { public: virtual ostream& print( ostream &os ) const; // ... |
ostream& AndQuery:: print( ostream &os ) const { if ( _lparen ) print_lparen( _lparen, os ); _lop->print( os ); os << " && "; _rop->print( os ); if ( _rparen ) print_rparen( _rparen, os ); return os; |
Такая реализация виртуальной функции print()
позволяет вывести любой подтип Query в поток класса ostream или любого другого, производного от него:
cout << "Был сформулирован запрос "; Query *pq = retrieveQuery(); |
Однако такой возможности недостаточно. Еще нужно уметь распечатывать любой производный от Query
тип, который уже есть или может появиться в будущем, с помощью оператора вывода из библиотеки iostream:
Query *pq = retrieveQuery(); cout << "В ответ на запрос " << *pq |
Мы не можем непосредственно предоставить виртуальный оператор вывода, поскольку они являются членами класса ostream. Вместо этого мы должны написать косвенную виртуальную функцию:
inline ostream& operator<<( ostream &os, const Query &q ) { // виртуальный вызов print() return q.print( os ); |
Строки
AndQuery query; // сформулировать запрос ... |
вызывают наш оператор вывода в ostream, который в свою очередь вызывает
q.print( os )
где q
привязано к объекту query
класса AndQuery, а os – к cout. Если бы вместо этого мы написали:
NameQuery query2( "Salinger" ); |
то была бы вызвана реализация print() из класса NameQuery. Обращение
Query *pquery = retrieveQuery(); |
приводит к вызову той функции print(), которая ассоциирована с объектом, адресуемым указателем pquery в данной точке выполнения программы.