17.5.1. Виртуальный ввод/вывод
17.5.1. Виртуальный ввод/вывод
Первая виртуальная операция, которую мы хотели реализовать, - это печать запроса на стандартный вывод либо в файл:
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
А так выглядит объявление:
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
" получены следующие результаты: " ;
Мы не можем непосредственно предоставить виртуальный оператор вывода, поскольку они являются членами класса 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 в данной точке выполнения программы.
17.5.2. Чисто виртуальные функции
С точки зрения кодирования основная задача, стоящая перед нами в связи с поддержкой пользовательских запросов, - это реализация зависимых от типа операций для каждого из возможных операторов. Для этого мы определили четыре конкретных типа классов: AndQuery, OrQuery и т.д. Однако с точки зрения проектирования наша цель - инкапсулировать обработку каждого вида запроса, спрятать за не зависящим от типа интерфейсом. Это позволит построить ядро приложения, которое не потребует изменений при добавлении или удалении типов.
Чтобы добиться этого, определим абстрактный тип класса Query. При этом мы не будем программировать разные типы пользовательских запросов, а лишь абстрактные операции, применимые к ним:
void doit_and_bedone( vector Query* *pvec )
{
vectorQuery*::iterator
it = pvec-begin(),
end_it = pvec-end();
for ( ; it != end_it; ++it )
{
Query *pq = *it;
cout "обрабатывается " *pq endl;
pq-eval();
pq-display();
delete pq;
}
}
Такое определение позволяет добавлять неограниченное число типов запросов без необходимости изменять или даже перекомпилировать ядро системы, но при условии, что открытый интерфейс нашего абстрактного базового класса Query достаточен для поддержки новых запросов.
Проектируя открытый интерфейс Query, мы определим множество операций, достаточное для поддержки всех существующих и будущих типов запросов, хотя на практике нам вряд ли удастся это гарантировать. Предоставление общего интерфейса для тех запросов, о которых мы уже знаем, - вполне реальная задача, но любое заявление, претендующее на более широкую поддержку, следует рассматривать с долей скептицизма.
Поскольку Query - абстрактный класс, объекты которого в приложении не создаются, то никакой разумной реализации виртуальных функций в нем самом мы предложить не можем. Это лишь названия, которые должны быть замещены в производных классах. Напрямую вызывать их мы не будем.
Язык обладает синтаксической конструкцией, обозначающей, что некоторая виртуальная функция предоставляет интерфейс, который должен быть замещен в производных подтипах, но вызываться непосредственно не может. Это чисто виртуальные функции. Объявляются они следующим образом:
class Query {
public:
// объявляется чисто виртуальная функция
virtual ostream& print( ostream&=cout ) const = 0;
// ...
};
Заметьте, что за объявлением функции следует присваивание нуля.
Класс, содержащий (или наследующий) одну или несколько таких функций, распознается компилятором как абстрактный базовый класс. Попытка создать независимый объект абстрактного класса приводит к ошибке компиляции. (Ошибкой является также вызов чисто виртуальной функции с помощью механизма виртуализации.) Например:
// В классе Query объявлены одна или несколько виртуальных функций,
// поэтому программист не может создавать независимые объекты
// класса Query
// правильно: подобъект Query в составе NameQuery
Query *pq = new NameQuery( " Nostromo" );
// ошибка: оператор new создает объект класса Query
Query *pq2 = new Query;
Абстрактный базовый класс может существовать только как подобъект в составе объекта некоторого производного от него класса. Это именно та семантика, которая нужна нам для базового Query.