Главная страница
Навигация по странице:

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

  • 17.5.6. Виртуальная функция eval()

  • Язык программирования C++. Вводный курс. С для начинающих


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница83 из 93
    1   ...   79   80   81   82   83   84   85   86   ...   93

    17.5.2.
    Чисто виртуальные функции
    С точки зрения кодирования основная задача, стоящая перед нами в связи с поддержкой пользовательских запросов, – это реализация зависимых от типа операций для каждого из возможных операторов. Для этого мы определили четыре конкретных типа классов:
    AndQuery
    , OrQuery и т.д. Однако с точки зрения проектирования наша цель – инкапсулировать обработку каждого вида запроса, спрятать за не зависящим от типа интерфейсом. Это позволит построить ядро приложения, которое не потребует изменений при добавлении или удалении типов.
    Чтобы добиться этого, определим абстрактный тип класса Query. При этом мы не будем программировать разные типы пользовательских запросов, а лишь абстрактные операции, применимые к ним:
    }
    NameQuery query2( "Salinger" );
    Query *pquery = retrieveQuery(); void doit_and_bedone( vector< Query* > *pvec )
    { vector::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;
    }

    С++ для начинающих
    889
    Такое определение позволяет добавлять неограниченное число типов запросов без необходимости изменять или даже перекомпилировать ядро системы, но при условии, что открытый интерфейс нашего абстрактного базового класса Query достаточен для поддержки новых запросов.
    Проектируя открытый интерфейс Query, мы определим множество операций, достаточное для поддержки всех существующих и будущих типов запросов, хотя на практике нам вряд ли удастся это гарантировать. Предоставление общего интерфейса для тех запросов, о которых мы уже знаем, – вполне реальная задача, но любое заявление, претендующее на более широкую поддержку, следует рассматривать с долей скептицизма.
    Поскольку Query – абстрактный класс, объекты которого в приложении не создаются, то никакой разумной реализации виртуальных функций в нем самом мы предложить не можем. Это лишь названия, которые должны быть замещены в производных классах.
    Напрямую вызывать их мы не будем.
    Язык обладает синтаксической конструкцией, обозначающей, что некоторая виртуальная функция предоставляет интерфейс, который должен быть замещен в производных подтипах, но вызываться непосредственно не может. Это чисто виртуальные функции.
    Объявляются они следующим образом:
    };
    Заметьте, что за объявлением функции следует присваивание нуля.
    Класс, содержащий (или наследующий) одну или несколько таких функций, распознается компилятором как абстрактный базовый класс. Попытка создать независимый объект абстрактного класса приводит к ошибке компиляции. (Ошибкой является также вызов чисто виртуальной функции с помощью механизма виртуализации.) Например:
    Query *pq2 = new Query;
    Абстрактный базовый класс может существовать только как подобъект в составе объекта некоторого производного от него класса. Это именно та семантика, которая нужна нам для базового Query. class Query { public:
    // объявляется чисто виртуальная функция virtual ostream& print( ostream&=cout ) const = 0;
    // ...
    //
    В классе Query объявлены одна или несколько виртуальных функций,
    // поэтому программист не может создавать независимые объекты
    // класса Query
    // правильно: подобъект Query в составе NameQuery
    Query *pq = new NameQuery( "Nostromo" );
    // ошибка: оператор new создает объект класса Query

    С++ для начинающих
    890
    17.5.3.
    Статический вызов виртуальной функции
    Вызывая виртуальную функцию с помощью оператора разрешения области видимости класса, мы отменяем механизм виртуализации и разрешаем вызов статически, на этапе компиляции. Предположим, что мы определили виртуальную функцию isA() в базовом и каждом из производных классов иерархии Query: pquery->Query::isA();
    Тогда явный вызов Query::isA() разрешается на этапе компиляции в пользу реализации isA()
    в базовом классе Query, хотя pquery адресует объект NameQuery.
    Зачем нужно отменять механизм виртуализации? Как правило, ради эффективности. В теле виртуальной функции производного класса часто необходимо вызвать реализацию из базового, чтобы завершить операцию, расщепленную между базовым и производным классами. К примеру, вполне вероятно, что виртуальная функция display() из Camera выводит некоторую информацию, общую для всех камер, а реализация display() в классе PerspectiveCamera сообщает информацию, специфичную только для перспективных камер. Вместо того чтобы дублировать в ней действия, общие для всех камер, можно вызвать реализацию из класса Camera. Мы точно знаем, какая именно реализация нам нужна, поэтому нет нужды прибегать к механизму виртуализации. Более того, реализация в Camera объявлена встроенной, так что разрешение во время компиляции приводит к подстановке по месту вызова.
    Приведем еще один пример, когда отмена механизма виртуализации может оказаться полезной, а заодно познакомимся с неким аспектом чисто виртуальных функций, который начинающим программистам кажется противоречащим интуиции.
    Реализации функции print() в классах AndQuery и OrQuery совпадают во всем, кроме литеральной строки, представляющей название оператора. Реализуем только одну функцию, которую можно вызывать из данных классов. Для этого мы снова определим абстрактный базовый BinaryQuery (его наследники – AndQuery и OrQuery). В нем определены два операнда и еще один член типа string для хранения значения оператора.
    Поскольку это абстрактный класс, объявим print() чисто виртуальной функцией:
    Query *pquery = new NameQuery( "dumbo" );
    // isA() вызывается динамически с помощью механизма виртуализации
    // реально будет вызвана NameQuery::isA() pquery->isA();
    // isA вызывается статически во время компиляции
    // реально будет вызвана Query::isA

    С++ для начинающих
    891
    };
    Вот как реализована в BinaryQuery функция print(), которая будет вызываться из производных классов AndQuery и OrQuery:
    }
    Похоже, мы попали в парадоксальную ситуацию. С одной стороны, необходимо объявить этот экземпляр print() как чисто виртуальную функцию, чтобы компилятор воспринимал BinaryQuery как абстрактный базовый класс. Тогда в приложении определить независимые объекты BinaryQuery будет невозможно.
    С другой стороны, нужно определить в классе BinaryQuery виртуальную функцию print()
    и уметь вызывать ее через объекты AndQuery и OrQuery.
    Но как часто бывает с кажущимися парадоксами, мы не учли одного обстоятельства: чисто виртуальную функцию нельзя вызывать с помощью механизма виртуализации, но можно вызывать статически:
    } 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; 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; inline ostream&
    AndQuery:: print( ostream &os ) const
    {
    // правильно: подавить механизм виртуализации
    // вызвать BinaryQuery::print статически
    BinaryQuery::print( os );

    С++ для начинающих
    892
    17.5.4.
    Виртуальные функции и аргументы по умолчанию
    Рассмотрим следующую простую иерархию классов:
    };
    Проектировщик класса хотел, чтобы при вызове без параметров реализации foo() из базового класса по умолчанию передавался аргумент 1024: pb->foo();
    Кроме того, разработчик хотел, чтобы при вызове его реализации foo() без параметров использовался аргумент по умолчанию 2048: pb->foo();
    Однако в C++ принята другая семантика механизма виртуализации. Вот небольшая программа для тестирования нашей иерархии классов:
    #include 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;
    }
    // ... base b; base *pb = &b;
    // вызывается base::foo( int )
    // предполагалось, что будет возвращено 1024 derived d; base *pb = &d;
    // вызывается derived::foo( int )
    // предполагалось, что будет возвращено 2048

    С++ для начинающих
    893
    }
    После компиляции и запуска программа выводит следующую информацию: 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(), зависел от фактически вызванного экземпляра? К сожалению, механизм виртуализации такую возможность не поддерживает. Однако разрешается задать такой аргумент по умолчанию, который для вызванной функции означает, что пользователь не передал никакого значения. Тогда реальное значение, которое функция хотела бы видеть в качестве аргумента по умолчанию, объявляется локальной переменной и используется, если ничего другого не передано: 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;

    С++ для начинающих
    894
    }
    Здесь base_default_value – значение, согласованное между всеми классами иерархии, которое явно говорит о том, что пользователь не передал никакого аргумента.
    Производный класс может быть реализован аналогично:
    }
    17.5.5.
    Виртуальные деструкторы
    В данной функции мы применяем оператор delete:
    }
    Чтобы функция выполнялась правильно, применение delete должно вызывать деструктор того класса, на который указывает pq. Следовательно, необходимо объявить деструктор Query виртуальным: void base:: foo( int ival = base_default_value )
    { int real_default_value = 1024; // настоящее значение по умолчанию if ( ival == base_default_value ) ival = real_default_value;
    // ... void derived:: foo( int ival = base_default_value )
    { int real_default_value = 2048; if ( ival == base_default_value ) ival = real_default_value;
    // ... void doit_and_bedone( vector< Query* > *pvec )
    {
    // ... for ( ; it != end_it; ++it )
    {
    Query *pq = *it;
    // ... delete pq;
    }

    С++ для начинающих
    895
    };
    Деструкторы всех производных от Query классов автоматически считаются виртуальными. doit_and_bedone() выполняется правильно.
    Поведение деструктора при наследовании таково: сначала вызывается деструктор производного класса, в случае pq – виртуальная функция. По завершении вызывается деструктор непосредственного базового класса – статически. Если деструктор объявлен встроенным, то в точке вызова производится подстановка. Например, если pq указывает на объект класса AndQuery, то delete pq; приводит к вызову деструктора класса AndQuery за счет механизма виртуализации.
    После этого статически вызывается деструктор BinaryObject, а затем – снова статически – деструктор Query.
    В следующей иерархии классов
    }; уровень доступа к конструктору NotQuery открытый при вызове через объект NotQuery, но защищенный – при вызове через указатель или ссылку на объект Query. Таким образом, виртуальная функция подразумевает уровень доступа того класса, через объект которого вызывается:
    } class Query { public: virtual Query() { delete _solution; }
    // ... class Query { public: // ... protected: virtual Query();
    // ...
    }; class NotQuery : public Query { public:
    NotQuery();
    // ... int main()
    {
    Query *pq = new NotQuery;
    // ошибка: деструктор является защищенным delete pq;

    С++ для начинающих
    896
    Эвристическое правило: если в корневом базовом классе иерархии объявлены одна или несколько виртуальных функций, рекомендуем объявлять таковым и деструктор. Однако, в отличие от конструктора базового класса, его деструктор не стоит делать защищенным.
    17.5.6.
    Виртуальная функция eval()
    В основе иерархии классов Query лежит виртуальная функция eval() (но с точки зрения возможностей языка она наименее интересна). Как и для других функций-членов, разумной реализации eval() в абстрактном классе Query нет, поэтому мы объявляем ее чисто виртуальной:
    };
    Реальное разрешение имени eval() происходит при построении отображения слов на вектор позиций. Если слово есть в тексте, то в отображении будет его вектор позиций. В нашей реализации вектор позиций, если он имеется, передается конструктору NameQuery вместе с самим словом. Поэтому в классе NameQuery функция eval() пуста.
    Однако мы не можем унаследовать чисто виртуальную функцию из Query. Почему?
    Потому что NameQuery – это конкретный класс, объекты которого разрешается создавать в приложении. Если бы мы унаследовали чисто виртуальную функцию, то он стал бы абстрактным классом, так что создать объект такого типа не удалось бы. Поэтому мы объявим eval() пустой функцией:
    };
    Для запроса NotQuery отыскиваются все строки текста, где указанное слово отсутствует.
    Для таких строк в член _loc класса NotQuery помещаются все пары (строка, колонка).
    Наша реализация выглядит следующим образом: class Query { public: virtual void eval() = 0;
    // ... class NameQuery : public Query { public: virtual void eval() {}
    // ...

    С++ для начинающих
    897
    }
    Ниже приводится трассировка выполнения запроса 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() мог упорядочить void NotQuery::eval()
    {
    // вычислим операнд
    _op->eval();
    // _all_locs - это вектор, содержащий начальные позиции всех слов,
    // он является статическим членом NotQuery:
    // static const vector* _all_locs vector< location >::const_iterator iter = _all_locs->begin(), iter_end = _all_locs->end();
    // получить множество строк, в которых операнд встречается set *ps = _vec2set( _op->locations() );
    // для каждой строки, где операнд не найден,
    // скопировать все позиции в _loc for ( ; iter != iter_end; ++iter )
    { if ( ! ps->count( (*iter).first )) {
    _loc.push_back( *iter );
    }
    }

    С++ для начинающих
    898
    пары (строка, колонка), мы определяем объект-функцию для их сравнения. Ниже приведена наша реализация:
    }
    А вот трассировка выполнения запроса 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. Основная трудность связана с тем, что эти векторы нужно просматривать синхронно, чтобы можно было установить соседство слов. 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() );

    С++ для начинающих
    1   ...   79   80   81   82   83   84   85   86   ...   93


    написать администратору сайта