Правила программирования на С и С++. Главы 7-8 - Шаблоны не заменяют наследование; они его автоматизируют
ОГЛАВЛЕНИЕ
157. Шаблоны классов должны обычно определять производные классы.
158. Шаблоны не заменяют наследование; они его автоматизируют.
Главное, что нужно запомнить о шаблонах классов, - это то, что они порождают много определений классов. Как и всякий раз, когда у вас есть множество сходных определений классов, идентичные функции должны быть соединены в общий базовый класс.
Во-первых, давайте взглянем на то, что не нужно делать. Класс storable, уже использованный мной, снова представляется хорошим примером. Сначала создадим объект collection для управления сохраняемыми объектами:
class collection{
storable *head;public: // ...storable *find( const storable ?a_match_of_this ) const;
};storable *collection::find( const storable ?a_match_of_this ) const
{
// Послать сообщение объекту начала списка, указывающее, что// список просматривается на совпадение со значением a_match_of_this;
return head ? head->find( a_match_of_this )
: NULL;
}Механизм поиска нужных объектов скрыт внутри класса storable. Вы можете изменить лежащую в основе структуру данных, поменяв определение storable, и эти изменения совсем не затронут реализацию класса collection.Затем давайте реализуем класс storable, использующий простой связанный список в качестве лежащей в основе структуры данных:
class storable{
storable *next, *prev;public: storable *find ( const storable ?match_of_this ) const;storable *successor ( void ) const;
virtual int operator== ( const storable ?r ) const;
};storable *storable::find( const storable ?match_of_this ) const
{
// Возвращает указатель на первый элемент в списке с корнем// на себя самого, имеющий тот же ключ, что и "r". Обычно,
// объект-коллекция должен послать это сообщение объекту начала
// списка, указатель на который хранится в классе коллекции.
storable *current = this;
for( ; current; current = current->next ) if( *current == match_of_this ) // найдено совпадение return current;}storable *storable::successor( void ) const
{
// Возвращает следующее значение в последовательности.return next;
}Функция operator==() должна быть чисто виртуальной, потому что отсутствует возможность ее реализации на уровне класса storable. Реализация должна быть выполнена в производном классе13 : class storable_string : public storable{
string s;public: virtual int operator==( const storable ?r ) const;// ...
};virtual int operator==( const storable ?r ) const
{
storable_string *right = dynamic_cast?storable_string *>( ?r );return right ? (s == r.s) : NULL;
}Я здесь использовал предложенный в ISO/ANSI C++ безопасный механизм нисходящего приведения типов. right инициализируется значением NULL, если передаваемый объект (r) не относится к типу storable_string. Например, он может принадлежать к некоторому другому классу, также являющемуся наследником storable.
Пока все идет хорошо. Теперь к проблемам, связанным с шаблонами. Кто-нибудь, не понимающий того, что делает, говорит: "Ребята, я могу исключить наследование и потребность в виртуальных функциях, используя шаблоны", а делает, вероятно, нечто подобное:
template ?class t_key>class storable
{
storable *next, *prev;
t_key key;
public:
// ...storable *find ( const storable ?match_me ) const;
storable *successor ( void ) const;
int operator==( const storable ?r ) const;
};template ?class t_key>
int storable?t_key>::operator==( const storable?t_key> ?r ) const
{
return key == r.key ;}template ?class t_key>
storable?t_key> *storable?t_key>::successor( void ) const
{
return next;}template ?class t_key>
storable *storable?t_key>::find( const storable?t_key> ?match_me ) const
{
storable?t_key> *current = this;for( ; current; current = current->next )
if( *current == match_me ) // найдено совпадение return current;}Проблема здесь в непроизводительных затратах. Функции-члены шаблона класса сами являются шаблонами функций. Когда компилятор расширяет шаблон storable, он также расширяет варианты всех функций-членов этого шаблона. Хотя я их не показал, вероятно, в классе storable определено множество функций. Многие из этих функций будут похожи на функцию successor() в том, что они не используют информацию о типе, передаваемую в шаблон. Это означает, что каждое расширение такой функции будет идентично по содержанию любому другому ее расширению. Из функций, которые не похожи на это, большинство будут подобны find(), использующей информацию о типе, но которые легко изменить так, чтобы ее не использовать.Вы можете решить эту проблему, использовав механизм шаблонов для создания производного класса. Основываясь на предыдущей реализации, не использующей шаблоны, вы можете сделать следующее:
template ?class t_key>class storable_tem : public storable
{
t_key key;public: virtual int operator==( const storable ?r ) const; // Замещение базового класса// ...
};template ?class t_key>
/* виртуальный */ int storable_tem?t_key>::operator==( const storable ?r ) const
{
t_key *right = dynamic_cast?t_key *>( ?r );return right ? (s == r.s) : NULL;
}Выбрав другой путь, я сосредоточил в базовом классе все функции, которые не зависят от типа key. Затем я использовал механизм шаблонов для создания определения производного класса, реализующего только те функции, которым нужно знать тип key.Полезным результатом является существенное сокращение размера кода. Механизм шаблонов может рассматриваться как средство автоматизации производства шаблонных производных классов.