Виртуальный ввод/вывод
Первая виртуальная операция, которую мы хотели реализовать, – это печать запроса на стандартный вывод либо в файл:
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();
pq->print( cout );
Однако такой возможности недостаточно. Еще нужно уметь распечатывать любой производный от Query тип, который уже есть или может появиться в будущем, с помощью оператора вывода из библиотеки iostream:
Query *pq = retrieveQuery();
cout << "В ответ на запрос "
<< *pq
<< " получены следующие результаты:\n";
Мы не можем непосредственно предоставить виртуальный оператор вывода, поскольку они являются членами класса ostream. Вместо этого мы должны написать косвенную виртуальную функцию:
inline ostream&
operator<<( ostream &os, const Query &q )
{
// виртуальный вызов print()
return q.print( os );
}
Строки
AndQuery query;
// сформулировать запрос ...
cout << query << endl;
вызывают наш оператор вывода в ostream, который в свою очередь вызывает
q.print( os )
где q привязано к объекту query класса AndQuery, а os – к cout. Если бы вместо этого мы написали:
NameQuery query2( "Salinger" );
cout << query2 << endl;
то была бы вызвана реализация print() из класса NameQuery. Обращение
Query *pquery = retrieveQuery();
cout << *pquery << endl;
приводит к вызову той функции print(), которая ассоциирована с объектом, адресуемым указателем pquery в данной точке выполнения программы.