НА ГЛАВНУЮ
Меню сайта
Категория
Ghost++ [1]
С++ [55]
Развлечение
ON - LINE
Опрос
Что урсе нужнее?
Всего ответов: 457
Оbserver Ward

Онлайн всего: 2
Гостей: 2
Пользователей: 0


Друзья сайта
Заведи себе Бота
Hаша кнопка
Для обмена банерами , наша кнопка для размещения у вас на сайте

Клансайт USSR


Главная » Статьи » Программирование » С++

17. Наследование и подтипизация классов (3)
17.5.3. Статический вызов виртуальной функции

Вызывая виртуальную функцию с помощью оператора разрешения области видимости класса, мы отменяем механизм виртуализации и разрешаем вызов статически, на этапе компиляции. Предположим, что мы определили виртуальную функцию isA() в базовом и каждом из производных классов иерархии Query:

Query *pquery = new NameQuery( " dumbo"  );
// isA() вызывается динамически с помощью механизма виртуализации
// реально будет вызвана NameQuery::isA()
pquery->isA();
// isA вызывается статически во время компиляции
// реально будет вызвана Query::isA
pquery->Query::isA();

Тогда явный вызов Query::isA() разрешается на этапе компиляции в пользу реализации isA() в базовом классе Query, хотя pquery адресует объект NameQuery.

Зачем нужно отменять механизм виртуализации? Как правило, ради эффективности. В теле виртуальной функции производного класса часто необходимо вызвать реализацию из базового, чтобы завершить операцию, расщепленную между базовым и производным классами. К примеру, вполне вероятно, что виртуальная функция display() из Camera выводит некоторую информацию, общую для всех камер, а реализация display() в классе PerspectiveCamera сообщает информацию, специфичную только для перспективных камер. Вместо того чтобы дублировать в ней действия, общие для всех камер, можно вызвать реализацию из класса Camera. Мы точно знаем, какая именно реализация нам нужна, поэтому нет нужды прибегать к механизму виртуализации. Более того, реализация в Camera объявлена встроенной, так что разрешение во время компиляции приводит к подстановке по месту вызова.

Приведем еще один пример, когда отмена механизма виртуализации может оказаться полезной, а заодно познакомимся с неким аспектом чисто виртуальных функций, который начинающим программистам кажется противоречащим интуиции.

Реализации функции print() в классах AndQuery и OrQuery совпадают во всем, кроме литеральной строки, представляющей название оператора. Реализуем только одну функцию, которую можно вызывать из данных классов. Для этого мы снова определим абстрактный базовый BinaryQuery (его наследники - AndQuery и OrQuery). В нем определены два операнда и еще один член типа string для хранения значения оператора. Поскольку это абстрактный класс, объявим print() чисто виртуальной функцией:

class BinaryQuery : public Query {
public:
   BinaryQuery( Query *lop, Query *rop, string oper )
              : _lop(lop), _rop(rop), _oper(oper) {}

   ~BinaryQuery() { delete _lop; delete _rop; }
   ostream &print( ostream&=cout, ) const = 0;

protected:
   Query *_lop;
   Query *_rop;
   string _oper;
};

Вот как реализована в BinaryQuery функция print(), которая будет вызываться из производных классов AndQuery и OrQuery:

inline ostream&
BinaryQuery::
print( ostream &os ) const
{
   if ( _lparen )
     print_lparen( _lparen, os );

   _lop->print( os );
   os < < ' ' < <  _oper < <  ' ';
   _rop->print( os );

   if ( _rparen )
     print_rparen( _rparen, os );

   return os;
}

Похоже, мы попали в парадоксальную ситуацию. С одной стороны, необходимо объявить этот экземпляр print() как чисто виртуальную функцию, чтобы компилятор воспринимал BinaryQuery как абстрактный базовый класс. Тогда в приложении определить независимые объекты BinaryQuery будет невозможно.

С другой стороны, нужно определить в классе BinaryQuery виртуальную функцию print() и уметь вызывать ее через объекты AndQuery и OrQuery.

Но как часто бывает с кажущимися парадоксами, мы не учли одного обстоятельства: чисто виртуальную функцию нельзя вызывать с помощью механизма виртуализации, но можно вызывать статически:

inline ostream&
AndQuery::
print( ostream &os ) const
{
   // правильно: подавить механизм виртуализации
   // вызвать BinaryQuery::print статически
   BinaryQuery::print( os );
}

17.5.4. Виртуальные функции и аргументы по умолчанию

Рассмотрим следующую простую иерархию классов:

#include < iostream>

class base {
public:
   virtual int foo( int ival = 1024 ) {
      cout < <  " base::foo() -- ival: " < <  ival < <  endl;
      return ival;
   }

   // ...
};

class derived : public base {
public:
   virtual int foo( int ival = 2048 ) {
      cout < <   " derived::foo() -- ival: "  < <   ival < <   endl;
      return ival;
   }

   // ...
};

Проектировщик класса хотел, чтобы при вызове без параметров реализации foo() из базового класса по умолчанию передавался аргумент 1024:

base b;
base *pb = &b;

// вызывается base::foo( int )
// предполагалось, что будет возвращено 1024
pb->foo();

Кроме того, разработчик хотел, чтобы при вызове его реализации foo() без параметров использовался аргумент по умолчанию 2048:

derived d;
base *pb = &d;

// вызывается derived::foo( int )
// предполагалось, что будет возвращено 2048
pb->foo();

Однако в C++ принята другая семантика механизма виртуализации. Вот небольшая программа для тестирования нашей иерархии классов:

int main()
{
   derived *pd = new derived;
   base *pb = pd;

   int val = pb->foo();
   cout < <   "main() : val через base: "
       < <  val < <   endl;

   val = pd->foo();
   cout < <   "main() : val через derived: "
        < <   val < <   endl;
}

После компиляции и запуска программа выводит следующую информацию:

derived::foo() -- ival: 1024
main() : val через base: 1024
derived::foo() -- ival: 2048
main() : val через derived: 2048

При обоих обращениях реализация foo() из производного класса вызывается корректно, поскольку фактически вызываемый экземпляр определяется во время выполнения на основе типа класса, адресуемого pd и pb. Но передаваемый foo() аргумент по умолчанию определяется не во время выполнения, а во время компиляции на основе типа объекта, через который вызывается функция. При вызове foo() через pb аргумент по умолчанию извлекается из объявления base::foo() и равен 1024. Если же foo() вызывается через pd, то аргумент по умолчанию извлекается из объявления derived::foo() и равен 2048.

Если реализации из производного класса при вызове через указатель или ссылку на базовый класс по умолчанию передается аргумент, указанный в базовом классе, то зачем задавать аргумент по умолчанию для реализации из производного класса?

Нам могут понадобиться различные аргументы по умолчанию в зависимости не от реализации foo() в конкретном производном классе, а от типа указателя или ссылки, через которые функция вызвана. Например, значения 1024 и 2048 - это размеры изображений. Когда нужно получить менее детальное изображение, вызываем foo() через класс base, а когда более детальное - через derived.

Но если мы все-таки хотим, чтобы аргумент по умолчанию, передаваемый foo(), зависел от фактически вызванного экземпляра? К сожалению, механизм виртуализации такую возможность не поддерживает. Однако разрешается задать такой аргумент по умолчанию, который для вызванной функции означает, что пользователь не передал никакого значения. Тогда реальное значение, которое функция хотела бы видеть в качестве аргумента по умолчанию, объявляется локальной переменной и используется, если ничего другого не передано:

void
base::
foo( int ival = base_default_value )
{
   int real_default_value = 1024;     // настоящее значение по умолчанию

   if ( ival == base_default_value )
      ival = real_default_value;

   // ...
}

Здесь base_default_value - значение, согласованное между всеми классами иерархии, которое явно говорит о том, что пользователь не передал никакого аргумента. Производный класс может быть реализован аналогично:

void
derived::
foo( int ival = base_default_value )
{
   int real_default_value = 2048;

   if ( ival == base_default_value )
      ival = real_default_value;

   // ...
}

17.5.5. Виртуальные деструкторы

В данной функции мы применяем оператор delete:

void doit_and_bedone( vector< Query* > *pvec )
{
   // ...
   for ( ; it != end_it; ++it )
   {
       Query *pq = *it;
       // ...
       delete pq;
   }
}

Чтобы функция выполнялась правильно, применение delete должно вызывать деструктор того класса, на который указывает pq. Следовательно, необходимо объявить деструктор Query виртуальным:

class Query {
public:
   virtual ~Query() { delete _solution; }
   // ...
};

Деструкторы всех производных от Query классов автоматически считаются виртуальными. doit_and_bedone() выполняется правильно.

Поведение деструктора при наследовании таково: сначала вызывается деструктор производного класса, в случае pq - виртуальная функция. По завершении вызывается деструктор непосредственного базового класса - статически. Если деструктор объявлен встроенным, то в точке вызова производится подстановка. Например, если pq указывает на объект класса AndQuery, то

delete pq;

приводит к вызову деструктора класса AndQuery за счет механизма виртуализации. После этого статически вызывается деструктор BinaryObject, а затем - снова статически - деструктор Query.

В следующей иерархии классов

class Query {
public:  // ...
protected:
   virtual ~Query();
   // ...
};
class NotQuery : public Query {
public:
   ~NotQuery();
   // ...
};

уровень доступа к конструктору NotQuery открытый при вызове через объект NotQuery, но защищенный - при вызове через указатель или ссылку на объект Query. Таким образом, виртуальная функция подразумевает уровень доступа того класса, через объект которого вызывается:

int main()
{
   Query *pq = new NotQuery;
   // ошибка: деструктор является защищенным
   delete pq;
}

Эвристическое правило: если в корневом базовом классе иерархии объявлены одна или несколько виртуальных функций, рекомендуем объявлять таковым и деструктор. Однако, в отличие от конструктора базового класса, его деструктор не стоит делать защищенным.
17.5.6. Виртуальная функция eval()

В основе иерархии классов Query лежит виртуальная функция eval() (но с точки зрения возможностей языка она наименее интересна). Как и для других функций-членов, разумной реализации eval() в абстрактном классе Query нет, поэтому мы объявляем ее чисто виртуальной:

class Query {
public:
   virtual void eval() = 0;
   // ...
};

Реальное разрешение имени eval() происходит при построении отображения слов на вектор позиций. Если слово есть в тексте, то в отображении будет его вектор позиций. В нашей реализации вектор позиций, если он имеется, передается конструктору NameQuery вместе с самим словом. Поэтому в классе NameQuery функция eval() пуста.

Однако мы не можем унаследовать чисто виртуальную функцию из Query. Почему? Потому что NameQuery - это конкретный класс, объекты которого разрешается создавать в приложении. Если бы мы унаследовали чисто виртуальную функцию, то он стал бы абстрактным классом, так что создать объект такого типа не удалось бы. Поэтому мы объявим eval() пустой функцией:

class NameQuery : public Query {
public:
   virtual void eval() {}
   // ...
};

Для запроса NotQuery отыскиваются все строки текста, где указанное слово отсутствует. Для таких строк в член _loc класса NotQuery помещаются все пары (строка, колонка). Наша реализация выглядит следующим образом:

void NotQuery::eval()
{
    // вычислим операнд
    _op->eval();

    // _all_locs - это вектор, содержащий начальные позиции всех слов,
    // он является статическим членом NotQuery:
    // static const vector<locations>* _all_locs
    vector< location >::const_iterator
            iter = _all_locs->begin(),
            iter_end = _all_locs->end();

    // получить множество строк, в которых операнд встречается
    set<short> *ps = _vec2set( _op->locations() );

    // для каждой строки, где операнд не найден,
    // скопировать все позиции в _loc
    for ( ; iter != iter_end; ++iter )
    {
      if ( ! ps->count( (*iter).first )) {
         _loc.push_back( *iter );    
      }
    }
}

Ниже приводится трассировка выполнения запроса NotQuery. Операнд встречается в 0, 3 и 5 строках текста. (Напомним, что внутри программы строки текста в векторе нумеруются с 0; а когда мы предъявляем строки пользователю, мы нумеруем их с единицы.) Поэтому при вычислении ответа создается вектор, содержащий начальные позиции слов в строках 1,2 и 4. (Мы отредактировали вектор позиций, чтобы он занимал меньше места.)

==> ! daddy
daddy ( 3 ) lines match
display_location_vector:
        first: 0          second: 8
        first: 3          second: 3
        first: 5          second: 5
! daddy ( 3 ) lines match
display_location_vector:
        first: 1          second: 0
        first: 1          second: 1
        first: 1          second: 2
        ...
        first: 1          second: 10
        first: 2          second: 0
        first: 2          second: 1
        ...
        first: 2          second: 12
        first: 4          second: 0
        first: 4          second: 1
        ...
        first: 4          second: 12

Requested query:    ! daddy
( 2 ) when the wind blows through her hair, it looks almost alive,
( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her,
( 5 ) she tells him, at the same time wanting him to tell her more.

При обработке запроса OrQuery векторы позиций обоих операндов объединяются. Для этого применяется обобщенный алгоритм merge(). Чтобы merge() мог упорядочить пары (строка, колонка), мы определяем объект-функцию для их сравнения. Ниже приведена наша реализация:

class less_than_pair {
public:
    bool operator()( location loc1, location loc2 )
    {
        return (( loc1.first < loc2.first ) ||
                   ( loc1.first == loc2.first ) &&
                   ( loc1.second < loc2.second ));
    }
};
void OrQuery::eval()
{
    // вычислить левый и правый операнды
    _lop->eval();
    _rop->eval();

    // подготовиться к объединению двух векторов позиций
    vector< location, allocator >::const_iterator
        riter = _rop->locations()->begin(),
        liter = _lop->locations()->begin(),
        riter_end = _rop->locations()->end(),
        liter_end = _lop->locations()->end();

    merge( liter, liter_end, riter, riter_end,
         inserter( _loc, _loc.begin() ),
         less_than_pair() );
}

А вот трассировка выполнения запроса OrQuery, в которой мы выводим вектор позиций каждого из двух операндов и результат их объединения. (Напомним еще раз, что для пользователя строки нумеруются с 1, а внутри программы - с 0.)

==> fiery || untamed
fiery ( 1 ) lines match
display_location vector:
        first: 2          second: 2
        first: 2          second: 8

untamed ( 1 ) lines match
display_location vector:
        first: 3          second: 2

fiery || untamed ( 2 ) lines match
display_location vector:
        first: 2          second: 2
        first: 2          second: 8
        first: 3          second: 2

Requested query: fiery || untamed
( 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,"

При обработке запроса AndQuery мы обходим векторы позиций обоих операндов и ищем соседние слова. Каждая найденная пара вставляется в вектор _loc. Основная трудность связана с тем, что эти векторы нужно просматривать синхронно, чтобы можно было установить соседство слов.

void AndQuery::eval()
{
    // вычислить левый и правый операнды
    _lop->eval();
    _rop->eval();

    // установить итераторы
    vector< location, allocator >::const_iterator
        riter = _rop->locations()->begin(),
        liter = _lop->locations()->begin(),
        riter_end = _rop->locations()->end(),
        liter_end = _lop->locations()->end();

    // продолжать цикл, пока есть что сравнивать
    while ( liter != liter_end &&
          riter != riter_end )
    {
      // пока номер строки в левом векторе больше, чем в правом
      while ( (*liter).first > (*riter).first )
      {
         ++riter;
         if ( riter == riter_end ) return;
      }

      // пока номер строки в левом векторе меньше, чем в правом
      while ( (*liter).first < (*riter).first )
      {
          // если соответствие найдено для последнего слова
          // в одной строке и первого слова в следующей
          // _max_col идентифицирует последнее слово в строке
         if ( ((*liter).first == (*riter).first-1 ) &&
            ((*riter).second == 0 ) &&
            ((*liter).second == (*_max_col)[ (*liter).first ] ))
         {
          _loc.push_back( *liter );
          _loc.push_back( *riter );
          ++riter;
          if ( riter == riter_end ) return;
         }
         ++liter;
         if ( liter == liter_end ) return;
      }

      // пока оба в одной и той же строке
      while ( (*liter).first == (*riter).first )
      {
         if ( (*liter).second+1 == ((*riter).second) )
         {  // соседние слова
            _loc.push_back( *liter ); ++liter;
            _loc.push_back( *riter ); ++riter;
         }
         else
         if ( (*liter).second <= (*riter).second )
            ++liter;
         else ++riter;
         if ( liter == liter_end || riter == riter_end )
           return;
      }
    }
}

А так выглядит трассировка выполнения запроса AndQuery, в которой мы выводим векторы позиций обоих операндов и результирующий вектор:

==> fiery && bird
fiery ( 1 ) lines match
display_location vector:
        first: 2           second: 2
        first: 2           second: 8
bird ( 1 ) lines match
display_location vector:
        first: 2           second: 3
        first: 2           second: 9
fiery && bird ( 1 ) lines match
display_location vector:
        first: 2           second: 2
        first: 2           second: 3
        first: 2           second: 8
        first: 2           second: 9

Requested query: fiery && bird
( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her,

Приведем трассировку выполнения составного запроса, включающего как И, так и ИЛИ. Показаны векторы позиций каждого операнда, а также результирующий вектор:

==> fiery && ( bird || untamed )
fiery ( 1 ) lines match
display_location vector:
        first: 2           second: 3
        first: 2           second: 8
bird ( 1 ) lines match
display_location vector:
        first: 2           second: 3
        first: 2           second: 9
untamed ( 1 ) lines match
display_location vector:
        first: 3          second: 2
( bird || untamed ) ( 2 ) lines match
display_location vector:
        first: 2           second: 3
        first: 2           second: 9
        first: 3           second: 2
fiery && ( bird || untamed ) ( 1 ) lines match
display_location vector:
        first: 2           second: 2
        first: 2           second: 3
        first: 2           second: 8
        first: 2           second: 9
Requested query: fiery && ( bird || untamed )
( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her,

17.5.7. Почти виртуальный оператор new

Если дан указатель на один из конкретных подтипов запроса, то разместить в хипе дубликат объекта несложно:

NotQuery *pnq;
// установить pnq ...

// оператор new вызывает
// копирующий конструктор NotQuery ...
NotQuery *pnq2 = new NotQuery( *pnq );

Если же у нас есть только указатель на абстрактный класс Query, то задача создания дубликата становится куда менее тривиальной:

const Query *pq = pnq->op();
// как получить дубликат pq?

Если бы позволялось объявить виртуальный экземпляр оператора new, то проблема была бы решена, поскольку автоматически вызывался бы нужный экземпляр. К сожалению, это невозможно: new - статическая функция-член, которая применяется к неструктурированной памяти еще до конструирования объекта класса (см. раздел 15.8).

Но хотя оператор new нельзя сделать виртуальным, разрешается создать его суррогат, который будет выделять память из хипа и копировать туда объекты, - clone():

class Query {
public:
   virtual Query *clone() = 0;
   // ...
};

Вот как он может быть реализован в классе NameQuery:

class NameQuery : public Query {
public:
   virtual Query *clone()
      // вызывается копирующий конструктор класса NameQuery
      { return new NameQuery( *this ); }

   // ...
};

Это работает правильно, если тип целевого указателя Query*:

Query *pq = new NameQuery( "valery" );
Query *pq2 = pq->clone();

Если же его тип равен NameQuery*, нужно привести возвращенный указатель типа Query* назад к типу NameQuery*:

NameQuery *pnq = new NameQuery( "Rilke" );
NameQuery *pnq2 =
    static_cast<NameQuery*>( pnq->clone() );

(Причина, по которой необходимо преобразование типа, объясняется в разделе 19.1.1.)

Как правило, тип значения, возвращаемого реализацией виртуальной функции в производном классе, должен совпадать с типом, возвращаемым ее реализацией в базовом. Исключение, о котором мы уже упоминали, призвано поддержать рассмотренную ситуацию. Если виртуальная функция в базовом классе возвращает значение некоторого типа класса (либо указатель или ссылку на тип класса), то ее реализация в производном может возвращать значение, тип которого является производным от этого класса с открытым типом наследования (то же относится к ссылкам и указателям):

class NameQuery : public Query {
public:
   virtual NameQuery *clone()
      { return new NameQuery( *this ); }

// ...
};

Теперь pq2 и pnq2 можно инициализировать без явного приведения типов:

// Query *pq = new NameQuery( "Broch" );
Query *pq2 = pq->clone();   // правильно
// NameQuery *pnq = new NameQuery( "Rilke" );
NameQuery *pnq2 = pnq->clone();   // правильно

Так выглядит реализация clone() в классе NotQuery:

class NotQuery : public Query {
public:
   virtual NotQuery *clone()
      { return new NotQuery( *this ); }

   // ...
};

Реализации в AndQuery и OrQuery аналогичны. Чтобы эти реализации clone() работали правильно, в классах NotQuery, AndQuery и OrQuery должны быть явно определены копирующие конструкторы. (Мы займемся этим в разделе 17.6.)
17.5.8. Виртуальные функции, конструкторы и деструкторы

Как мы видели в разделе 17.4, для объекта производного класса сначала вызывается конструктор базового, а затем производного класса. Например, при таком определении объекта NameQuery

NameQuery poet( "Orlen" );

сначала будет вызван конструктор Query, а потом NameQuery.

При выполнении конструктора базового класса Query часть объекта, соответствующая классу NameQuery, остается неинициализированной. По существу, poet - это еще не объект NameQuery, сконструирован лишь его подобъект.

Что должно происходить, если внутри конструктора базового класса вызывается виртуальная функция, реализации которой существуют как в базовом, так и в производном классах? Какая из них должна быть вызвана? Результат вызова реализации из производного класса в случае, когда необходим доступ к его членам, оказался бы неопределенным. Вероятно, выполнение программы закончилось бы крахом.

Чтобы этого не случилось, в конструкторе базового класса всегда вызывается реализация виртуальной функции, определенная именно в базовом. Иными словами, внутри такого конструктора объект производного класса рассматривается как имеющий тип базового.

То же самое справедливо и внутри деструктора базового класса, вызываемого для объекта производного. И в этом случае часть объекта, относящаяся к производному классу, не определена: не потому, что еще не сконструирована, а потому, что уже уничтожена.

Упражнение 17.12

Внутри объекта NameQuery естественное внутреннее представление вектора позиций - это указатель, который инициализируется указателем, хранящимся в отображении слов. Оно же является и наиболее эффективным, так как нам нужно скопировать лишь один адрес, а не каждую пару координат. Классы AndQuery, OrQuery и NotQuery должны конструировать собственные векторы позиций на основе вычисления своих операндов. Когда время жизни объекта любого из этих классов завершается, ассоциированный с ним вектор позиций необходимо удалить. Когда же заканчивается время жизни объекта NameQuery, вектор позиций удалять не следует. Как сделать так, чтобы вектор позиций был представлен указателем в базовом классе Query и при этом его экземпляры для объектов AndQuery, OrQuery и NotQuery удалялись, а для объектов NameQuery - нет? (Заметим, что нам не разрешается добавить в класс Query признак, показывающий, нужно ли применять оператор delete к вектору позиций!)

Упражнение 17.13

Что неправильно в приведенном определении класса:

class AbstractObject {
public:
   ~AbstractObject();
   virtual void doit() = 0;
   // ...
};

Упражнение 17.14

Даны такие определения:

NameQuery nq( "Sneezy" );
Query q( nq );
Query *pq = &nq;

Почему в инструкции

pq->eval();

вызывается экземпляр eval() из класса NameQuery, а в инструкции

q.eval();

экземпляр из Query?

Упражнение 17.15

Какие из повторных объявлений виртуальных функций в классе Derived неправильны:

(a) Base* Base::copy( Base* );
    Base* Derived::copy( Derived* );
(b) Base* Base::copy( Base* );
    Derived* Derived::copy( Vase* );
(c) ostream& Base::print( int, ostream&=cout );
    ostream& Derived::print( int, ostream& );
(d) void Base::eval() const;
    void Derived::eval();

Упражнение 17.16

Маловероятно, что наша программа заработает при первом же запуске и в первый раз, когда прогоняется с реальными данными. Средства отладки полезно включать уже на этапе проектирования классов. Реализуйте в нашей иерархии классов Query виртуальную функцию debug(), которая будет отображать члены соответствующих классов. Поддержите управление уровнем детализации двумя способами: с помощью аргумента, передаваемого функции debug(), и с помощью члена класса. (Последнее позволяет включать или отключать выдачу отладочной информации в отдельных объектах.)

Упражнение 17.17

Найдите ошибку в следующей иерархии классов:

class Object {
public:
   virtual void doit() = 0;
   // ...
protected:
   virtual ~Object();
};

class MyObject : public Object {
public:
   MyObject( string isA );
   string isA() const;
protected:
   string _isA;
};

17.6. Почленная инициализация и присваивание A

При проектировании класса мы должны позаботиться о том, чтобы почленная инициализация (см. раздел 14.6) и почленное присваивание (см. раздел 14.7) были реализованы правильно и эффективно. Рассмотрим связь этих операций с наследованием.

До сих пор мы не занимались явной обработкой почленной инициализации. Посмотрим, что происходит в нашей иерархии классов Query по умолчанию.

В абстрактном базовом классе Query определены три нестатических члена:

class Query {
public: // ...
protected:
   int _paren;
   set<short> *_solition;
   vector<location> _loc;
   // ...
};

Член _solution, если он установлен, адресует множество, память для которого выделена в хипе функцией-членом _vec2set(). Деструктор Query применяет к _solution оператор delete.

Класс Query должен предоставлять как явный копирующий конструктор, так и явный копирующий оператор присваивания. (Если вам это непонятно, перечитайте раздел 14.6.) Но сначала посмотрим, как почленное копирование по умолчанию происходит без них.

Производный класс NameQuery содержит объект-член типа string и подобъект базового Query. Если есть объект folk класса NameQuery:

NameQuery folk( "folk" );

то инициализация music с помощью folk

NameQuery music = folk;

осуществляется так:

Компилятор проверяет, есть ли в NameQuery явный копирующий конструктор. (Его нет. Поэтому необходимо применить почленную инициализацию по умолчанию.)

Далее компилятор проверяет, содержит ли объект NameQuery подобъекты базового класса. (Да, в нем имеется подобъект Query.)

Компилятор проверяет, определен ли в классе Query явный копирующий конструктор. (Нет, поэтому компилятор применит почленную инициализацию по умолчанию.)

Компилятор проверяет, содержит ли объект Query подобъекты базового класса. (Нет.)

Компилятор просматривает все нестатические члены Query в порядке их объявления. (Если некоторый член не является объектом класса, как, например, _paren и _solution, то в объекте music он инициализируется соответствующим членом объекта folk. Если же является, как, скажем, _loc, то к нему рекурсивно применяется шаг 1. В классе vector определен копирующий конструктор, который вызывается для инициализации music._loc с помощью folk._loc.)

Далее компилятор рассматривает нестатические члены NameQuery в порядке их объявления и находит объект класса string, где есть явный копирующий конструктор. Он и вызывается для инициализации music._name с помощью folk._name.

Инициализация по умолчанию music с помощью folk завершена. Она хороша во всех отношениях, кроме одного: если разрешить копирование по умолчанию члена _solution, то программа, скорее всего, завершится аварийно. Поэтому вместо такой обработки мы предоставим явный копирующий конструктор класса Query. Можно, например, скопировать все разрешающее множество:

Query::Query( const Query &rhs )
            : _loc( rhs._loc ), _paren(rhs._paren)
{
   if ( rhs._solution )
   {
      _solution = new set<short>;
      set<short>::iterator
                  it = rhs._solution->begin(),
                  end_it = rhs._solution->end();

      for ( ; _ir != end_it; ++it )
          _solution->insert( *it );
   }
   else _solution = 0;
}

Однако, поскольку в нашей реализации разрешающее множество вычисляется по мере необходимости, копировать его сразу нет нужды. Назначение нашего копирующего конструктора - предотвратить копирование по умолчанию. Для этого достаточно инициализировать _solution нулем:

Query::Query( const Query &rhs )
            : _loc( rhs._loc ),
              _paren(rhs._paren), _solution( 0 )
{}

Шаги 1 и 2 инициализации musiс c помощью folk те же, что и раньше. Но на шаге 3 компилятор обнаруживает, что в классе Query есть явный копирующий конструктор и вызывает его. Шаги 4 и 5 пропускаются, а шаг 6 выполняется, как и прежде.

На этот раз почленная инициализация music с помощью folk корректна. Реализовывать явный копирующий конструктор в NameQuery нет необходимости.

Объект производного класса NotQuery содержит подобъект базового Query и член _op типа Query*, который указывает на операнд, размещенный в хипе. Деструктор NotQuery применяет к этому операнду оператор delete.

Для класса NotQuery почленная инициализация по умолчанию члена _op небезопасна, поэтому необходим явный копирующий конструктор. В его реализации используется виртуальная функция clone(), которую мы определили в предыдущем разделе.

inline NotQuery::
NotQuery( const NotQuery &rhs )
        // вызывается Query::Query( const Query &rhs )
        : Query( rhs )
        { _op = rhs._op->clone(); }

При почленной инициализации одного объекта класса NotQuery другим выполняются два шага:

Компилятор проверяет, определен ли в NotQuery явный копирующий конструктор. Да, определен.

Этот конструктор вызывается для почленной инициализации.

Вот и все. Ответственность за правильную инициализацию подобъекта базового класса и нестатических членов возлагается на копирующий конструктор NotQuery. (Классы AndQuery и OrQuery сходны с NotQuery, поэтому мы оставляем их в качестве упражнения для читателей.)

Почленное присваивание аналогично почленной инициализации. Если имеется явный копирующий оператор присваивания, то он вызывается для выполнения присваивания одного объекта класса другому. В противном случае применяется почленное присваивание по умолчанию.

Если базовый класс есть, то сначала с помощью копирующего оператора присваивания почленно присваивается подобъект данного класса, иначе такое присваивание рекурсивно применяется к базовым классам и членам подобъекта базового класса.

Просматриваются все нестатические члены в порядке их объявления. Если член не является объектом класса, то его значение справа от знака равенства копируется в значение соответствующего члена слева от знака равенства. Если же член является объектом класса, в котором определен явный копирующий оператор присваивания, то он и вызывается. В противном случае к базовым классам и членам объекта-члена применяется почленное присваивание по умолчанию.

Вот как выглядит копирующий оператор присваивания для нашего объекта Query. Еще раз отметим, что в этом месте необязательно копировать разрешающее множество, достаточно предотвратить копирование по умолчанию:

Query&
Query::
operator=( const Query &rhs )
{
   // предотвратить присваивание самому себе
   if ( &rhs != this )
   {
      _paren = rhs._paren;
      _loc = rhs._loc;
      delete _solution;
      _solution = 0;
   }

   return *this;
};

В классе NameQuery явный копирующий оператор присваивания не нужен. Присваивание одного объекта NameQuery другому выполняется в два шага:

Для присваивания подобъектов Query двух объектов NameQuery вызывается явный копирующий оператор присваивания класса Query.

Для присваивания членов string вызывается явный копирующий оператор присваивания этого класса.

Для объектов NameQuery вполне достаточно почленного присваивания по умолчанию.

В каждом из классов NotQuery, AndQuery и OrQuery для безопасного копирования операндов требуется явный копирующий оператор присваивания. Вот его реализация для NotQuery:

inline NotQuery&
NotQuery::
operator=( const NotQuery &rhs )
{
   // предотвратить присваивание самому себе
   if ( &rhs != this )
   {
       // вызвать копирующий оператор присваивания Query
      this->Query::operator=( rhs );

       // скопировать операнд
      _op = rhs._op->clone();
   }

   return *this;
}

В отличие от копирующего конструктора, в копирующем операторе присваивания нет специальной части, через которую вызывается аналогичный оператор базового класса. Для этого используются две синтаксических конструкции: явный вызов, продемонстрированный выше, и явное приведение типа, как в следующем примере:

(*static_cast<Query*>(this)) = rhs;

(Реализация копирующих операторов присваивания в классах AndQuery и OrQuery выглядит так же, поэтому мы оставим ее в качестве упражнения.)

Ниже предложена небольшая программа для тестирования данной реализации. Мы создаем или копируем объект, а затем распечатываем его.

#include "
Query.h"
int
main()
{
   NameQuery nm( "alice" );
   NameQuery nm( "emma" );

   NotQuery nq1( &nm );
   cout < "notQuery 1: " <<      nq1 <<      endl;

   NotQuery nq2( nq1 );
   cout <  "notQuery 2: " <<      nq2 <<      endl;
   NotQuery nq3( &nm2 );
   cout <  "notQuery 3: " <<      nq3 <<      endl;

   nq3 = nq2;
   cout <<     "notQuery 3 присвоено значение nq2: " << nq3 << endl;

   AndQuery aq( &nq1, &nm2 );
   cout <<      "AndQuery : " <<      aq <<      endl;

   AndQuery aq2( aq );
   cout <<      "AndQuery 2: " <<      aq2 <<      endl;

   AndQuery aq3( &nm, &nm2 );
   cout <<      "AndQuery 3: " <<      aq3 <<      endl;

   aq2 = aq3;
   cout <<      "AndQuery 2 после присваивания: " <<      aq2 <<      endl;
}

После компиляции и запуска программа печатает следующее:

notQuery 1:  ! alice
notQuery 2:  ! alice
notQuery 3:  ! emma
notQuery 3 присвоено значение nq2:  ! alice
AndQuery :  ! alice && emma
AndQuery 2:  ! alice && emma
AndQuery 3:  alice && emma
AndQuery 2 после присваивания:  alice && emma

Упражнение 17.18

Реализуйте копирующие конструкторы в классах AndQuery и OrQuery.

Упражнение 17.19

Реализуйте копирующие операторы присваивания в классах AndQuery и OrQuery.

Упражнение 17.20

Что указывает на необходимость реализации явных копирующего конструктора и копирующего оператора присваивания?
17.7. Управляющий класс UserQuery

Если имеется запрос такого типа:

fiery && ( bird || potato )

то в нашу задачу входит построение эквивалентной иерархии классов:

AndQuery
   NameQuery( "fiery" )
   OrQuery
      NameQuery( "bird" )
      NameQuery( "potato" )

Как лучше всего это сделать? Процедура вычисления ответа на запрос напоминает функционирование конечного автомата. Мы начинаем с пустого состояния и при обработке каждого элемента запроса переходим в новое состояние, пока весь запрос не будет разобран. В основе нашей реализации лежит одна инструкция switch внутри операции, которую мы назвали eval_query(). Слова запроса считываются одно за другим из вектора строк и сравниваются с каждым из возможных значений:

vector<string>::iterator
    it     = _query->begin(),
    end_it = _query->end();

for ( ; it != end_it; ++it )
     switch( evalQueryString( *it ))
     {
        case WORD:
             evalWord( *it );
           break;

        case AND:
           evalAnd();
           break;

        case OR:
           evalOr();
           break;

        case NOT:
           evalNot();
           break;

        case LPAREN:
           ++_paren;
           ++_lparenOn;
           break;

        case RPAREN:
           --_paren;
           ++_rparenOn;
           evalRParen();
           break;
     }

Пять операций eval: evalWord(), evalAnd(), evalOr(), evalNot и evalRParen() - как раз и строят иерархию классов Query. Прежде чем обратиться к деталям их реализации, рассмотрим общую организацию программы.

Нам нужно определить каждую операцию в виде отдельной функции, как это было сделано в главе 6 при построении процедур обработки запроса. Пользовательский запрос и производные от Query классы представляют независимые данные, которыми оперируют эти функции. От такой модели программирования (она называется процедурной) мы предпочли отказаться.

В разделе 6.14 мы ввели класс TextQuery, где инкапсулировали операции и данные, изучавшиеся в главе 6. Здесь нам потребуется класс UserQuery, решающий аналогичные задачи.

Одним из членов этого класса должен быть вектор строк, содержащий сам запрос пользователя. Другой член - это указатель типа Query* на иерархическое представление запроса, построенное в eval_query(). Еще три члена служат для обработки скобок:

_paren помогает изменить подразумеваемый порядок вычисления операторов (чуть позже мы продемонстрируем это на примере);

_lparenOn и _rparenOn содержат счетчики левых и правых скобок, ассоциированные с текущим узлом дерева разбора запроса (мы показывали, как они используются, при обсуждении виртуальной функции print() в разделе 17.5.1).

Помимо этих пяти членов, нам понадобятся еще два. Рассмотрим следующий запрос:

fiery || untamed

Наша цель - представить его в виде следующего объекта OrQuery:

OrQuery
   NameQuery( "fiery" )
   NameQuery( "untamed" )

Однако порядок обработки такого запроса вызывает некоторые проблемы. Когда мы определяем объект NameQuery, объект OrQuery , к которому его надо добавить, еще не определен. Поэтому необходимо место, где можно временно сохранить объект NameQuery.

Чтобы сохранить что-либо для последующего использования, традиционно применяется стек. Поместим туда наш объект NameQuery. А когда позже встретим оператор ИЛИ (объект OrQuery), то достанем NameQuery из стека и присоединим его к OrQuery в качестве левого операнда.

Объект OrQuery неполон: в нем не хватает правого операнда. До тех пор пока этот операнд не будет построен, работу с данным объектом придется прекратить.

Его можно поместить в тот же самый стек, что и NameQuery. Однако OrQuery представляет другое состояние обработки запроса: это неполный оператор. Поэтому мы определим два стека: _query_stack для хранения объектов, представляющих сконструированные операнды составного запроса (туда мы помещаем объект NameQuery), а второй для хранения неполных операторов с отсутствующим правым операндом. Второй стек можно трактовать как место для хранения текущей операции, подлежащей завершению, поэтому назовем его _current_op. Сюда мы и поместим объект OrQuery. После того как второй объект NameQuery будет определен, мы достанем объект OrQuery из стека _current_op и добавим к нему NameQuery в качестве правого операнда. Теперь объект OrQuery завершен и мы можем поместить его в стек _query_stack.

Если обработка запроса завершилась нормально, то стек _current_op пуст, а в стеке _query_stack содержится единственный объект, который и представляет весь пользовательский запрос. В нашем случае это объект класса OrQuery.

Рассмотрим несколько примеров. Первый из них - простой запрос типа NotQuery:

! daddy

Ниже показана трассировка его обработки. Финальным объектом в стеке _query_stack является объект класса NotQuery:

evalNot() : incomplete!
      push on _current_op ( size == 1 )
evalWord() : daddy
      pop _current_op : NotQuery
      add operand: WordQuery : NotQuery complete!
      push NotQuery on _query_stack

Текст, расположенный с отступом под функциями eval, показывает, как выполняется операция.

Во втором примере - составном запросе типа OrQuery - встречаются оба случая. Здесь же иллюстрируется помещение полного оператора в стек _query_stack:

==> fiery || untamed || shyly

evalWord() : fiery
      push word on _query_stack
evalOr() : incomplete!
      pop _query_stack : fiery
      add operand : WordQuery : OrQuery incomplete!
      push OrQuery on _current_op ( size == 1 )
evalWord() : untamed
      pop _current_op : OrQuery
      add operand : WordQuery : OrQuery complete!
      push OrQuery on _query_stack
evalOr() : incomplete!
      pop _query_stack : OrQuery
      add operand : OrQuery : OrQuery incomplete!
      push OrQuery on _current_op ( size == 1 )
evalWord() : shyly
      pop _current_op : OrQuery
      add operand : WordQuery : OrQuery complete!
      push OrQuery on _query_stack

В последнем примере рассматривается составной запрос и применение скобок для изменения порядка вычислений:

==> fiery && ( bird || untamed )
evalWord() : fiery
      push word on _query_stack
evalAnd() : incomplete!
      pop _query_stack : fiery
      add operand : WordQuery : AndQuery incomplete!
      push AndQuery on _current_op ( size == 1 )
evalWord() : bird
      _paren is set to 1
      push word on _query_stack
evalOr() : incomplete!
      pop _query_stack : bird
      add operand : WordQuery : OrQuery incomplete!
      push OrQuery on _current_op ( size == 2 )
evalWord() : untamed
      pop _current_op : OrQuery
      add operand : WordQuery : OrQuery complete!
      push OrQuery on _query_stack
evalRParen() :
      _paren: 0 _current_op.size(): 1
      pop _query_stack : OrQuery
      pop _current_op : AndQuery
      add operand : OrQuery : AndQuery complete!
      push AndQuery on _query_stack

Реализация системы текстового поиска состоит из трех компонентов:

класс TextQuery, где производится обработка текста (подробно он рассматривался в разделе 16.4). Для него нет производных классов;

объектно-ориентированная иерархия Query для представления и обработки различных типов запросов;

класс UserQuery, с помощью которого представлен конечный автомат для построения иерархии Query.

До настоящего момента мы реализовали эти три компонента практически независимо друг от друга и без каких бы то ни было конфликтов. Но, к сожалению, иерархия классов Query не поддерживает требований к конструированию объектов, предъявляемых реализацией UserQuery:

классы AndQuery, OrQuery и NotQuery требуют, чтобы каждый операнд присутствовал в момент определения объекта. Однако принятая нами схема обработки подразумевает наличие неполных объектов;

наша схема предполагает отложенное добавление операнда к объектам AndQuery, OrQuery и NotQuery. Более того, такая операция должна быть виртуальной. Операнд приходится добавлять через указатель типа Query*, находящийся в стеке _current_op. Однако способ добавления операнда зависит от типа: для унарных (NotQuery) и бинарных (AndQuery и OrQuery) операций он различен. Наша иерархия классов Query подобные операции не поддерживает.

Оказалось, что анализ предметной области был неполон, в результате чего разработанный интерфейс не согласуется с конкретной реализацией проекта. Нельзя сказать, что анализ был неправильным, он просто неполон. Эта проблема связана с тем, что этапы анализа, проектирования и реализации отделены друг от друга и не допускают обратной связи и пересмотра. Но хотя мы не можем все продумать и все предвидеть, необходимо отличать неизбежные неправильные шаги от ошибок, обусловленных собственной невнимательностью или нехваткой времени.

В таком случае мы должны либо сами модифицировать иерархию классов Query, либо договориться, чтобы это сделали за нас. В данной ситуации мы, как авторы всей системы, сами изменим код, модифицировав конструкторы подтипов и включив виртуальную функцию-член add_op() для добавления операндов после определения оператора (мы покажем, как она применяется, чуть ниже, при рассмотрении функций evalRParen() и evalWord()).
Категория: С++ | Добавил: r2d2 (29.09.2011)
Просмотров: 796 | Рейтинг: 0.0/0
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Born in Ussr
Залогиниться
Турниры

/j clan ussr /j clan cccp