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

  • 16.3.1. Функции-члены шаблонов Queue и QueueItem

  • 16.4.1. Объявления друзей в шаблонах Queue и QueueItem

  • 16.8.1. Модель компиляции с включением

  • 16.8.2. Модель компиляции с разделением

  • 16.8.3. Явные объявления конкретизации

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


    Скачать 5.41 Mb.
    НазваниеС для начинающих
    Дата24.08.2022
    Размер5.41 Mb.
    Формат файлаpdf
    Имя файлаЯзык программирования C++. Вводный курс.pdf
    ТипДокументы
    #652350
    страница76 из 93
    1   ...   72   73   74   75   76   77   78   79   ...   93

    795
    Queue( ) { front = back = 0; }
    За первым вхождением Queue (перед оператором ::) следует список параметров, показывающий, какому шаблону принадлежит данная функция-член. Второе вхождение
    Queue в определение конструктора (после оператора ::) содержит имя функции-члена, за которым может следовать список параметров шаблона, хотя это и необязательно. После имени функции идет ее определение;. в нем могут быть ссылки на параметр шаблона
    Type всюду, где в определении обычной функции использовалось бы имя типа.
    Функция-член шаблона класса сама является шаблоном. Стандарт C++ требует, чтобы она конкретизировалась только при вызове либо при взятии ее адреса. (Некоторые более старые компиляторы конкретизируют такие функции одновременно с конкретизацией самого шаблона класса.) При конкретизации функции-члена используется тип того объекта, для которого функция вызвана:
    Queue qs;
    Объект qs имеет тип Queue. При инициализации объекта этого класса вызывается конструктор Queue. В данном случае аргументом, которым конкретизируется функция-член (конструктор), будет string.
    Функция-член шаблона конкретизируется только при реальном использовании в программе (т.е. при вызове или взятии ее адреса). От того, в какой именно момент конкретизируется функция-член, зависит разрешение имен в ее определении (см. раздел
    16.11) и объявление ее специализации (см. раздел 16.9).
    16.3.1.
    Функции-члены шаблонов Queue и QueueItem
    Чтобы понять, как определяются и используются функции-члены шаблонов классов, продолжим изучение шаблонов Queue и QueueItem: template class Queue { public:
    Queue(); private:
    // ...
    }; template inline Queue::

    С++ для начинающих
    796
    };
    Деструктор, а также функции-члены remove() и add() определены не в теле шаблона, а вне его. Деструктор Queue опустошает очередь:
    }
    Функция-член Queue::add() помещает новый элемент в конец очереди:
    }
    Функция-член Queue::remove() возвращает значение элемента, находящегося в начале очереди, и удаляет сам элемент. template class Queue { public:
    Queue() : front( 0 ), back ( 0 ) { }

    Queue();
    Type& remove(); void add( const Type & ); bool is_empty() const { return front == 0;
    } private:
    QueueItem *front;
    QueueItem *back; template
    Queue::Queue()
    { while (! is_empty() ) remove(); template void Queue::add( const Type &val )
    {
    // создать новый объект QueueItem
    QueueItem *pt = new QueueItem( val ); if ( is_empty() ) front = back = pt; else
    { back->next = pt; back = pt;
    }

    С++ для начинающих
    797
    }
    Мы поместили определения функций-членов в заголовочный файл Queue.h, включив его в каждый файл, где возможны конкретизации функций. (Обоснование этого решения, а также рассмотрение более общих вопросов, касающихся модели компиляции шаблонов, мы отложим до раздела 16.8.)
    В следующей программе иллюстрируется использование и конкретизация функции-члена шаблона Queue:
    }
    После компиляции и запуска программа выводит следующую строку:
    #include
    #include template
    Type Queue::remove()
    { if ( is_empty() )
    { cerr << "remove() вызвана для пустой очереди\n"; exit( -1 );
    }
    QueueItem *pt = front; front = front->next;
    Type retval = pt->item; delete pt; return retval;
    #include
    #include "Queue.h" int main()
    {
    // конкретизируется класс Queue
    // оператор new требует, чтобы Queue был определен
    Queue *p_qi = new Queue; int ival; for ( ival = 0; ival < 10; ++ival )
    // конкретизируется функция-член add() p_qi->add( ival ); int err_cnt = 0; for ( ival = 0; ival < 10; ++ival ) {
    // конкретизируется функция-член remove() int qval = p_qi->remove(); if ( ival != qval ) err_cnt++;
    } if ( !err_cnt ) cout << "!! queue executed ok\n"; else cerr << "?? queue errors: " << err_cnt << endl; return 0;

    С++ для начинающих
    798
    !! queue executed ok
    Упражнение 16.5
    Используя шаблон класса Screen, определенный в разделе 16.2, реализуйте функции- члены Screen (см. разделы 13.3, 13.4 и 13.6) в виде функций-членов шаблона.
    16.4.
    Объявления друзей в шаблонах классов

    С++ для начинающих
    799

    обычный (не шаблонный) дружественный класс или дружественная функция. В следующем примере функция foo(), функция-член bar() и класс foobar объявлены друзьями всех конкретизаций шаблона QueueItem:
    };
    Ни класс foobar, ни функцию foo() не обязательно объявлять или определять в глобальной области видимости перед объявлением их друзьями шаблона
    QueueItem
    Однако перед тем как объявить другом какой-либо из членов класса Foo, необходимо определить его. Напомним, что член класса может быть введен в область видимости только через определение объемлющего класса. QueueItem не может ссылаться на Foo::bar(), пока не будет найдено определение Foo;

    связанный дружественный шаблон класса или функции. В следующем примере определено взаимно однозначное соответствие между классами, конкретизированными по шаблону QueueItem, и их друзьями – также конкретизациями шаблонов. Для каждого класса, конкретизированного по шаблону QueueItem, ассоциированные конкретизации foobar, foo() и
    Queue::bar()
    являются друзьями.
    }; class Foo { void bar();
    }; template class QueueItem { friend class foobar; friend void foo(); friend void Foo::bar();
    // ... template class foobar { ... }; template void foo( QueueItem ); template class Queue { void bar();
    // ...
    }; template class QueueItem { friend class foobar; friend void foo( QueueItem ); friend void Queue::bar();
    // ...

    С++ для начинающих
    800
    Прежде чем шаблон класса можно будет использовать в объявлениях друзей, он сам должен быть объявлен или определен. В нашем примере шаблоны классов foobar и Queue, а также шаблон функции foo() следует объявить до того, как они объявлены друзьями в QueueItem.
    Синтаксис, использованный для объявления foo() другом, может показаться странным: friend void foo( QueueItem );
    За именем функции следует список явных аргументов шаблона: foo.
    Такой синтаксис показывает, что в качестве друга объявляется конкретизированный шаблон функции foo(). Если бы список явных аргументов был опущен: friend void foo( QueueItem ); то компилятор интерпретировал бы объявление как относящееся к обычной функции (а не к шаблону), у которой тип параметра – это экземпляр шаблона
    QueueItem
    . Как отмечалось в разделе 10.6, шаблон функции и одноименная обычная функция могут сосуществовать, и присутствие объявления такого шаблона перед определением класса QueueItem не вынуждает компилятор соотнести объявление друга именно с ним. Для того, чтобы соотнесение было верным, в конкретизированном шаблоне функции необходимо указать список явных аргументов;

    несвязанный дружественный шаблон класса или функции. В следующем примере имеется отображение один-ко-многим между конкретизациями шаблона класса QueueItem и его друзьями. Для каждой конкретизации типа QueueItem все конкретизации foobar, foo() и Queue::bar() являются друзьями:
    };
    Следует отметить, что этот вид объявлений друзей в шаблоне класса не поддерживается компиляторами, написанными до принятия стандарта C++.
    16.4.1.
    Объявления друзей в шаблонах Queue и QueueItem
    Поскольку QueueItem не предназначен для непосредственного использования в вызывающей программе, то объявление конструктора этого класса помещено в закрытую template class QueueItem { template friend class foobar; template friend void foo( QueueItem ); template friend class Queue::bar();
    // ...

    С++ для начинающих
    801
    секцию шаблона. Теперь класс Queue необходимо объявить другом QueueItem, чтобы можно было создавать и манипулировать объектами последнего.
    Существует два способа объявить шаблон класса другом. Первый заключается в том, чтобы объявить любой экземпляр Queue другом любого экземпляра QueueItem:
    };
    Однако нет смысла объявлять, например, класс Queue, конкретизированный типом string
    , другом
    QueueItem
    , конкретизированного типом complex
    Queue
    должен быть другом только для класса QueueItem. Таким образом, нам нужно взаимно однозначное соответствие между экземплярами Queue и
    QueueItem
    , конкретизированными одинаковыми типами. Чтобы добиться этого, применим второй метод объявления друзей:
    };
    Данное объявление говорит о том, что для любой конкретизации QueueItem некоторым типом экземпляр Queue, конкретизированный тем же типом, является другом. Так, экземпляр Queue, конкретизированный типом int, будет другом экземпляра QueueItem, тоже конкретизированного типом int
    Но для экземпляров
    QueueItem
    , конкретизированных типами complex или string, этот экземпляр Queue другом не будет.
    В любой точке программы у пользователю может понадобиться распечатать содержимое объекта Queue. Такая возможность предоставляется с помощью перегруженного оператора вывода. Этот оператор должен быть объявлен другом шаблона Queue, так как ему необходим доступ к закрытым членам класса. Какой же будет его сигнатура? ostream& operator<<( ostream &, ??? );
    Поскольку Queue – это шаблон класса, то в имени конкретизированного экземпляра должен быть задан полный список аргументов: ostream& operator<<( ostream &, const Queue & ); template class QueueItem {
    // любой экземпляр Queue является другом
    // любого экземпляра QueueItem template friend class Queue; template class QueueItem {
    // для любого экземпляра QueueItem другом является
    // только конкретизированный тем же типом экземпляр Queue friend class Queue;
    // ...
    // как задать аргумент типа Queue?

    С++ для начинающих
    802
    Так мы определили оператор вывода для класса, конкретизированного из шаблона Queue типом int. Но что, если Queue – это очередь элементов типа string? ostream& operator<<( ostream &, const Queue & );
    Вместо того чтобы явно определять нужный оператор вывода по мере необходимости, желательно сразу определить общий оператор, который будет работать для любой конкретизации Queue. Например: ostream& operator<<( ostream &, const Queue & );
    Однако из этого перегруженного оператора вывода придется сделать шаблон функции: operator<<( ostream &, const Queue & );
    Теперь всякий раз, когда оператору ostream передается конкретизированный экземпляр
    Queue
    , конкретизируется и вызывается шаблон функции. Вот одна из возможных реализаций оператора вывода в виде такого шаблона:
    }
    Если очередь объектов типа int содержит значения 3, 5, 8, 13, то распечатка ее содержимого с помощью такого оператора дает
    < 3 5 8 13 >
    Обратите внимание, что оператор вывода обращается к закрытому члену front класса
    Queue
    . Поэтому оператор необходимо объявить другом Queue:
    };
    Здесь, как и при объявлении друга в шаблоне класса Queue, создается взаимно однозначное соответствие между конкретизациями Queue и оператора operator<<(). template ostream& template ostream& operator<<( ostream &os, const Queue &q )
    { os << "< ";
    QueueItem *p; for ( p = q.front; p; p = p->next ) os << *p << " "; os << " >"; return os; template class Queue { friend ostream& operator<<( ostream &, const Queue & );
    // ...

    С++ для начинающих
    803
    Распечатка элементов Queue производится оператором вывода operator<<() класса
    QueueItem
    : os << *p;
    Этот оператор также должен быть реализован в виде шаблона функции; тогда можно быть уверенным, что в нужный момент будет конкретизирован подходящий экземпляр:
    }
    Поскольку здесь имеется обращение к закрытому члену item класса QueueItem, оператор следует объявить другом шаблона QueueItem. Это делается следующим образом:
    };
    Оператор вывода класса QueueItem полагается на то, что item умеет распечатывать себя: os << qi.item;
    Это порождает тонкую зависимость типов при конкретизации Queue. Любой определенный пользователем и связанный с Queue класс, содержимое которого нужно распечатывать, должен предоставлять оператор вывода. В языке нет механизма, с помощью которого можно было бы задать такую зависимость в определении самого шаблона Queue. Но если оператор вывода не определен для типа, с которым конкретизируется данный шаблон, и делается попытка вывести содержимое конкретизированного экземпляра, то в том месте, где используется отсутствующий оператор вывода, компилятор выдает сообщение об ошибке. Шаблон Queue можно конкретизировать типом, не имеющим оператора вывода, – при условии, что не будет попытки распечатать содержимое очереди.
    Следующая программа демонстрирует конкретизацию и использование функций-друзей шаблонов классов Queue и QueueItem: template ostream& operator<<( ostream &os, const QueueItem &qi )
    { os << qi.item; return os; template class QueueItem { friend class Queue; friend ostream& operator<<( ostream &, const QueueItem & );
    // ...

    С++ для начинающих
    804
    }
    После компиляции и запуска программа выдает результат:
    < >
    < 0 1 2 3 4 5 6 7 8 9 >
    < >
    !! queue executed ok
    Упражнение 16.6
    Пользуясь шаблоном класса Screen, определенным в упражнении 16.5, реализуйте операторы ввода и вывода (см. упражнение 15.6 из раздела 15.2) в виде шаблонов.
    Объясните, почему вы выбрали тот, а не иной способ объявления друзей класса Screen, добавленных в его шаблон.
    16.5.
    Статические члены шаблонов класса
    В шаблоне класса могут быть объявлены статические данные-члены. Каждый конкретизированный экземпляр имеет собственный набор таких членов. Рассмотрим операторы new() и delete() для шаблона QueueItem. В класс QueueItem нужно добавить два статических члена: static const unsigned QueueItem_chunk;
    Модифицированное определение шаблона QueueItem выглядит так:
    #include
    #include "Queue.h" int main() {
    Queue qi;
    // конкретизируются оба экземпляра
    // ostream& operator<<(ostream &os, const Queue &)
    // ostream& operator<<(ostream &os, const QueueItem &) cout << qi << endl; int ival; for ( ival = 0; ival < 10; ++ival ) qi.add( ival ); cout << qi << endl; int err_cnt = 0; for ( ival = 0; ival < 10; ++ival ) { int qval = qi.remove(); if ( ival != qval ) err_cnt++;
    } cout << qi << endl; if ( !err_cnt ) cout << "!! queue executed ok\n"; else cout << "?? queue errors: " << err_cnt << endl; return 0; static QueueItem *free_list;

    С++ для начинающих
    805
    };
    Операторы new() и delete() объявлены закрытыми, чтобы предотвратить создание объектов типа QueueItem вызывающей программой: это разрешается только членам и друзьям QueueItem (к примеру, шаблону Queue).
    Оператор new() можно реализовать таким образом:
    }
    А реализация оператора delete() выглядит так:
    }
    Теперь остается инициализировать статические члены free_list и QueueItem_chunk.
    Вот шаблон для определения статических данных-членов:
    #include template class QueueItem {
    // ... private: void *operator new( size_t ); void operator delete( void *, size_t );
    // ... static QueueItem *free_list; static const unsigned QueueItem_chunk;
    // ... template void*
    QueueItem::operator new( size_t size )
    {
    QueueItem *p; if ( ! free_list )
    { size_t chunk = QueueItem_chunk * size; free_list = p = reinterpret_cast< QueueItem* >
    ( new char[chunk] ); for ( ; p != &free_list[ QueueItem_chunk - 1 ]; ++p ) p->next = p + 1; p->next = 0;
    } p = free_list; free_list = free_list->next; return p; template void QueueItem:: operator delete( void *p, size_t )
    { static_cast< QueueItem* >( p )->next = free_list; free_list = static_cast< QueueItem* > ( p );

    С++ для начинающих
    806
    QueueItem::QueueItem_chunk = 24;
    Определение шаблона статического члена должно быть вынесено за пределы определения самого шаблона класса, которое начинается с ключевого слово template с последующим списком параметров . Имени статического члена предшествует префикс
    QueueItem::
    , показывающий, что этот член принадлежит именно шаблону
    QueueItem
    . Определения таких членов помещаются в заголовочный файл Queue.h и должны включаться во все файлы, где производится их конкретизация. (В разделе 16.8 мы объясним, почему решили делать именно так, и затронем другие вопросы, касающиеся модели компиляции шаблонов.)
    Статический член конкретизируется по шаблону только в том случае, когда реально используется в программе. Сам такой член тоже является шаблоном. Определение шаблона для него не приводит к выделению памяти: она выделяется только для конкретизированного экземпляра статического члена. Каждая подобная конкретизация соответствует конкретизации шаблона класса. Таким образом, обращение к экземпляру статического члена всегда производится через некоторый конкретизированный экземпляр класса: int ival2 = QueueItem::QueueItem_chunk; // правильно
    Упражнение 16.7
    Реализуйте определенные в разделе 15.8 операторы new() и delete() и относящиеся к ним статические члены screenChunk и freeStore для шаблона класса Screen, построенного в упражнении 16.6.
    16.6.
    Вложенные типы шаблонов классов
    Шаблон класса QueueItem применяется только как вспомогательное средство для реализации Queue. Чтобы запретить любое другое использование, в шаблоне QueueItem имеется закрытый конструктор, позволяющий создавать объекты этого класса исключительно функциям-членам класса Queue, объявленным друзьями QueueItem. Хотя шаблон QueueItem виден во всей программе, создать объекты этого класса или обратиться к его членам можно только при посредстве функций-членов Queue.
    /* для каждой конкретизации QueueItem сгенерировать
    * соответствующий free_list и инициализировать его нулем
    */ template
    QueueItem *QueueItem::free_list = 0;
    /* для каждой конкретизации QueueItem сгенерировать
    * соответствующий QueueItem_chunk и инициализировать его значением 24
    */ template const unsigned int
    // ошибка: QueueItem - это не реальный конкретизированный экземпляр int ival0 = QueueItem::QueueItem_chunk; int ival1 = QueueItem::QueueItem_chunk; // правильно

    С++ для начинающих
    807
    Альтернативный подход к реализации состоит в том, чтобы вложить определение шаблона класса QueueItem в закрытую секцию шаблона Queue. Поскольку QueueItem является вложенным закрытым типом, он становится недоступным вызывающей программе, и обратиться к нему можно лишь из шаблона класса Queue и его друзей
    (например, оператора вывода). Если же сделать члены QueueItem открытыми, то объявлять Queue другом QueueItem не понадобится.
    Семантика исходной реализации при этом сохраняется, но отношение между шаблонами
    QueueItem и Queue моделируется более элегантно.
    Поскольку при любой конкретизации шаблона Queue требуется конкретизировать тем же типом и QueueItem, то вложенный класс должен быть шаблоном. Вложенные классы шаблонов сами являются шаблонами классов, а параметры объемлющего шаблона можно использовать во вложенном:
    };
    При каждой конкретизации Queue создается также класс QueueItem с подходящим аргументом для Type. Между конкретизациями шаблонов QueueItem и Queue имеется взаимно однозначное соответствие.
    Вложенный в шаблон класс конкретизируется только в том случае, если он используется в контексте, где требуется полный тип класса. В разделе 16.2 мы упоминали, что конкретизация шаблона класса Queue типом int не означает автоматической конкретизации и класса QueueItem. Члены front и back – это указатели на
    QueueItem
    , а если объявлены только указатели на некоторый тип, то конкретизировать соответствующий класс не обязательно, хотя QueueItem вложен в шаблон класса Queue. QueueItem конкретизируется только тогда, когда указатели front или back разыменовываются в функциях-членах класса Queue.
    Внутри шаблона класса можно также объявлять перечисления и определять типы (с помощью typedef): template class Queue:
    // ... private: class QueueItem { public:
    QueueItem( Type val )
    : item( val ), next( 0 ) { ... }
    Type item;
    QueueItem *next;
    };
    // поскольку QueueItem - вложенный тип,
    // а не шаблон, определенный вне Queue,
    // то аргумент шаблона после QueueItem можно опустить
    QueueItem *front, *back;
    // ...

    С++ для начинающих
    808
    }
    Вместо того чтобы явно включать член Buf_size, в шаблоне класса Buffer объявляется перечисление с двумя элементами, которые инициализируются значением параметра шаблона. Например, объявление
    Buffer small_buf; устанавливает Buf_size в 512, а last – в 511. Аналогично
    Buffer medium_buf; устанавливает Buf_size в 1024, а last – в 1023.
    Открытый вложенный тип разрешается использовать и вне определения объемлющего класса. Однако вызывающая программа может ссылаться лишь на конкретизированные экземпляры подобного типа (или элементов вложенного перечисления). В таком случае имени вложенного типа должно предшествовать имя конкретизированного шаблона класса:
    Buffer::Buf_vals bfv1; // правильно
    Это правило применимо и тогда, когда во вложенном типе не используются параметры включающего шаблона: template class Buffer: public: enum Buf_vals { last = size-1, Buf_size }; typedef Type BufType;
    BufType array[ size ];
    // ...
    // ошибка: какая конкретизация Buffer?
    Buffer::Buf_vals bfv0;

    С++ для начинающих
    809
    }
    Во всех конкретизациях Q значения empty одинаковы, но при ссылке на empty необходимо указывать, какому именно экземпляру Q принадлежит перечисление.
    Упражнение 16.8
    Определите класс List и вложенный в него ListItem из раздела 13.10 как шаблоны.
    Реализуйте аналогичные определения для ассоциированных членов класса.
    16.7.
    Шаблоны-члены
    Шаблон функции или класса может быть членом обычного класса или шаблона класса.
    Определение шаблона-члена похоже на определение шаблона: ему предшествует ключевое слово template, за которым идет список параметров: template class Q { public: enum QA { empty, full }; // не зависит от параметров
    QA status;
    // ...
    };
    #include int main() {
    Q qd;
    Q qi; qd.status = Q::empty; // ошибка: какая конкретизация Q? qd.status = Q::empty; // правильно int val1 = Q::empty; int val2 = Q::empty; if ( val1 != val2 ) cerr << "
    ошибка реализации!" << endl; return 0;

    С++ для начинающих
    810
    }
    (Отметим, что шаблоны-члены не поддерживаются компиляторами, написанными до принятия стандарта C++. Эта возможность была добавлена в язык для поддержки реализации абстрактных контейнерных типов, представленных в главе 6.)
    Объявление шаблона-члена имеет собственные параметры. Например, у шаблона класса
    CL
    есть параметр Type, а у шаблона функции assign() – параметр Iter. Помимо этого, в определении шаблона-члена могут использоваться параметры объемлющего шаблона класса. Например, у шаблона CL есть член типа T, представляющего параметр включающего шаблона Queue.
    Объявление шаблона-члена в шаблоне класса Queue означает, что конкретизация Queue потенциально может содержать бесконечное число различных вложенных классов CL функций-членов assign(). Так, конкретизированный экземпляр Queue включает вложенные типы:
    Queue::CL и вложенные функции: vector::iterator )
    Для шаблона-члена действуют те же правила доступа, что и для других членов класса.
    Так как шаблон CL является закрытым членом шаблона Queue, то лишь функции-члены и друзья Queue могут ссылаться на его конкретизации. С другой стороны, шаблон функции assign()
    объявлен открытым членом и, значит, доступен во всей программе. template class Queue { private:
    // шаблон класса-члена template class CL
    {
    Type member;
    T mem;
    };
    // ... public:
    // шаблон функции-члена template void assign( Iter first, Iter last )
    { while ( ! is_empty() ) remove(); // вызывается Queue::remove() for ( ; first != last; ++first ) add( *first ); // вызывается Queue::add( const T & )
    }
    Queue::CL void Queue::assign( int *, int * ) void Queue::assign( vector::iterator,

    С++ для начинающих
    811
    Шаблон-член конкретизируется при его использовании в программе. Например, assign()
    конкретизируется в момент обращения к ней из main():
    }
    Шаблон функции assign(), являющийся членом шаблона класса Queue, иллюстрирует необходимость применения шаблонов-членов для поддержки контейнерных типов.
    Предположим, имеется очередь типа Queue, в которую нужно поместить содержимое любого другого контейнера (списка, вектора или обычного массива), причем его элементы имеют либо тип int (т.е. тот же, что у элементов очереди), либо приводимый к типу int. Шаблон-член assign()позволяет это сделать. Поскольку может быть использован любой контейнерный тип, то интерфейс assign() программируется в расчете на употребление итераторов; в результате реализация оказывается не зависящей от фактического типа, на который итераторы указывают.
    В функции main() шаблон-член assign() сначала конкретизируется типом int*, что позволяет поместить в qi содержимое массива элементов типа int. Затем шаблон-член конкретизируется типом vector::iterator – это дает возможность поместить в очередь qi содержимое вектора элементов типа int. Контейнер, содержимое которого помещается в очередь, не обязательно должен состоять из элементов типа int. Разрешен любой тип, который приводится к int. Чтобы понять, почему это так, еще раз посмотрим на определение assign():
    }
    Вызываемая из assign() функция add() – это функция-член Queue::add().
    Если Queue конкретизируется типом int, то у add() будет следующий прототип: void Queue::add( const int &val );
    Аргумент *first должен иметь тип int либо тип, которым можно инициализировать параметр-ссылку на const int. Преобразования типов допустимы. Например, если int main()
    {
    // конкретизация Queue
    Queue qi;
    // конкретизация Queue::assign( int *, int * ) int ai[4] = { 0, 3, 6, 9 }; qi.assign( ai, ai + 4 );
    // конкретизация Queue::assign( vector::iterator,
    // vector::iterator ) vector vi( ai, ai + 4 ); qi.assign( vi.begin(), vi.end() ); template void assign( Iter first, Iter last )
    {
    // удалить все элементы из очереди for ( ; first != last; ++first ) add( *first );

    С++ для начинающих
    812
    воспользоваться классом SmallInt из раздела 15.9, то содержимое контейнера, в котором хранятся элементы типа SmallInt, с помощью шаблона-члена assign() помещается в очередь типа Queue. Это возможно потому, что в классе SmallInt имеется конвертер для приведения SmallInt к int:
    }
    Первая конкретизация assign() правильна, так как существует неявное преобразование из типа SmallInt в тип int и, следовательно, обращение к add() корректно. Вторая же конкретизация ошибочна: объект типа int* не может инициализировать ссылку на тип const int
    , поэтому вызвать функцию add() невозможно.
    Для контейнерных типов из стандартной библиотеки C++ имеется функция assign(), которая ведет себя так же, как функция-шаблон assign() для нашего класса Queue.
    Любую функцию-член можно задать в виде шаблона. Это относится, в частности, к конструктору. Например, для шаблона класса Queue его можно определить следующим образом: class SmallInt { public:
    SmallInt( int ival = 0 ) : value( ival ) { }
    // конвертер: SmallInt ==> int operator int() { return value; }
    // ... private: int value;
    }; int main()
    {
    // конкретизация Queue
    Queue qi; vector vsi;
    // заполнить вектор
    // конкретизация
    // Queue::assign( vector::iterator,
    // vector::iterator ) qi.assign( vsi.begin(), vsi.end() ); list lpi;
    // заполнить список
    // ошибка при конкретизации шаблона-члена assign():
    // нет преобразования из int* в int qi.assign( lpi.begin(), lpi.end() );

    С++ для начинающих
    813
    };
    Такой конструктор позволяет инициализировать очередь содержимым другого контейнера. У контейнерных типов из стандартной библиотеки C++ также есть предназначенные для этой цели конструкторы в виде шаблонов-членов. Кстати, в первом
    (в данном разделе) определении функции main() использовался конструктор-шаблон для вектора: vector vi( ai, ai + 4 );
    Это определение конкретизирует шаблон конструктора для контейнера vector типом int*, что позволяет инициализировать вектор содержимым массива элементов типа int.
    Шаблон-член, как и обычные члены, может быть определен вне определения объемлющего класса или шаблона класса. Так, являющиеся членами шаблон класса CL или шаблон функции assign() могут быть следующим образом определены вне шаблона
    Queue
    : template class Queue {
    // ... public:
    // шаблон-член конструктора template
    Queue( Iter first, Iter last )
    : front( 0 ), back( 0 )
    { for ( ; first != last; ++first ) add( * first );
    }

    С++ для начинающих
    814
    }
    Определению шаблона-члена, которое находится вне определения объемлющего шаблона класса, предшествует список параметров объемлющего шаблона класса, а за ним должен следовать собственный такой список. Вот почему определение шаблона функции assign()
    (члена шаблона класса Queue) начинается с template template
    Первый список параметров шаблона template относится к шаблону класса
    Queue
    . Второй – к самому шаблону-члену assign(). Имена параметров не обязаны совпадать с теми, которые указаны внутри определения объемлющего шаблона класса.
    Приведенная инструкция по-прежнему определяет шаблон-член assign(): template template
    { ... }
    16.8.
    Шаблоны классов и модель компиляции A
    Определение шаблона класса – это лишь предписание для построения бесконечного множества типов классов. Сам по себе шаблон не определяет никакого класса. Например, когда компилятор видит: template class Queue { private: template class CL;
    // ... public: template void assign( Iter first, Iter last );
    // ...
    }; template template class Queue::CL
    {
    Type member;
    T mem;
    }; template template void Queue::assign( Iter first, Iter last )
    { while ( ! is_empty() ) remove(); for ( ; first != last; ++first ) add( *first ); void Queue::assign( IterType first, IterType last )

    С++ для начинающих
    815
    class Queue { ... }; он только сохраняет внутреннее представление Queue. Позже, когда встречается реальное использование класса, конкретизированного по шаблону, скажем:
    } компилятор конкретизирует тип класса Queue, применяя сохраненное внутреннее представление определения шаблона Queue.
    Шаблон конкретизируется только тогда, когда он употребляется в контексте, требующем полного определения класса. (Этот вопрос подробно обсуждался в разделе 16.2.) В примере выше класс Queue конкретизируется, потому что компилятор должен знать размер типа Queue, чтобы выделить нужный объем памяти для объекта, созданного оператором new.
    Компилятор может конкретизировать шаблон только тогда, когда он видел не только его объявление, но и фактическое определение, которое должно предшествовать тому месту программы, где этот шаблон используется:
    }
    Шаблон класса можно конкретизировать одним и тем же типом в нескольких файлах.
    Как и в случае с типами классов, когда определение класса должно присутствовать в каждом файле, где используются его члены, компилятор конкретизирует шаблон некоторым типом во всех файлах, в которых данный экземпляр употребляется в контексте, требующем полного определения класса. Чтобы определение шаблона было доступно везде, где может понадобиться конкретизация, его следует поместить в заголовочный файл.
    Функции-члены и статические данные-члены шаблонов классов, а также вложенные в них типы ведут себя почти так же, как сами шаблоны. Определения членов шаблона используются для порождения экземпляров членов в конкретизированном шаблоне. Если компилятор видит: template int main() {
    Queue *p_qi = new Queue;
    // объявление шаблона класса template class Queue;
    Queue* global_pi = 0; // правильно: определение класса не нужно int main() {
    // ошибка: необходима конкретизация
    // определение шаблона класса должно быть видимо
    Queue *p_qi = new Queue;

    С++ для начинающих
    816
    { ... } он сохраняет внутреннее представление Queue::add(). Позже, когда в программе встречается фактическое употребление этой функции-члена, допустим через объект типа
    Queue
    , компилятор конкретизирует Queue::add(const int &), пользуясь таким представлением:
    }
    Конкретизация шаблона класса некоторым типом не приводит к автоматической конкретизации всех его членов тем же типом. Член конкретизируется только при использовании в таком контексте, где необходимо его определение (т.е. вложенный тип употреблен так, что требуется его полное определение; вызвана функция-член или взят ее адрес; имеется обращение к значению статического члена).
    Конкретизация функций-членов и статических членов шаблонов класса поднимает те же вопросы, которые мы уже обсуждали для шаблонов функций в разделе 10.5. Чтобы компилятор мог конкретизировать функцию-член или статический член шаблона класса, должно ли определение члена быть видимым в момент конкретизации? Например, должно ли определение функции-члена add() появиться до ее конкретизации типом int в main()? Следует ли помещать определения функций-членов и статических членов шаблонов класса в заголовочные файлы (как мы поступаем с определениями встроенных функций), которые включаются всюду, где применяются их конкретизированные экземпляры? Или конкретизации определения шаблона достаточно для того, чтобы этими членами можно было пользоваться, так что определения членов можно оставлять в файлах с исходными текстами (где обычно располагаются определения невстроенных функций-членов и статических членов)?
    Для ответа на эти вопросы нам придется вспомнить модель компиляции шаблонов в C++, где формулируются требования к организации программы, в которой определяются и употребляются шаблоны. Обе модели (с включением и с разделением), описанные в разделе 10.5, в полной мере применимы и к определениям функций-членов и статических данных-членов шаблонов классов. В оставшейся части этого раздела описываются обе модели и объясняется их использование с определениями членов.
    16.8.1.
    Модель компиляции с включением
    В этой модели мы включаем определения функций-членов и статических членов шаблонов классов в каждый файл, где они конкретизируются. Для встроенных функций- членов, определенных в теле шаблона, это происходит автоматически. В противном template void Queue::add( const Type &val )
    #include "Queue.h" int main() {
    // конкретизация Queue
    Queue *p_qi = new Queue; int ival;
    // ...
    // конкретизация Queue::add( const int & ) p_qi->add( ival );
    // ...

    С++ для начинающих
    817
    случае такое определение следует поместить в один заголовочный файл с определением шаблона класса. Именно этой моделью мы и пользуемся в настоящей книге. Например, определения шаблонов Queue и QueueItem, как и их функций-членов и статических членов, находятся в заголовочном файле Queue.h.
    Подобное размещение не лишено недостатков: определения функций-членов могут быть довольно большими и содержать детали реализации, которые неинтересны пользователям или должны быть скрыты от них. Кроме того, многократная компиляция одного определения шаблона при обработке разных файлов увеличивает общее время компиляции программы. Описанная модель (если она доступна) позволяет отделить интерфейс шаблона от реализации (т.е. от определений функций-членов и статических данных-членов).
    16.8.2.
    Модель компиляции с разделением
    В этой модели определение шаблона класса и определения встроенных функций-членов помещаются в заголовочный файл, а определения невстроенных функций-членов и статических данных-членов – в файл с исходным текстом программы. Иными словами, определения шаблона класса и его членов организованы так же, как определения обычных классов (не шаблонов) и их членов:
    };
    Type& Queue::remove() { ... }
    Программа, в которой используется конкретизированная функция-член, должна перед конкретизацией включить заголовочный файл:
    // ---- Queue.h ----
    // объявляет Queue как экспортируемый шаблон класса export template class Queue {
    // ... public:
    Type& remove(); void add( const Type & );
    // ...
    // ---- Queue.C ----
    // экспортированное определение шаблона класса Queue
    // находится в Queue.h
    #include "Queue.h" template void Queue::add( const Type &val ) { ... } template

    С++ для начинающих
    818
    }
    Хотя определение шаблона для функции-члена add() не видно в файле User.C, конкретизированный экземпляр Queue::add(const int &) вызывать оттуда можно. Но для этого шаблон класса необходимо объявить экспортируемым.
    Если он экспортируется, то для использования конкретизированных функций-членов или статических данных-членов необходимо знать лишь определение самого шаблона.
    Определения членов могут отсутствовать в тех файлах, где они конкретизируются.
    Чтобы объявить шаблон класса экспортируемым, перед словом template в его определении или объявлении нужно поставить ключевое слово export: class Queue { ... };
    В нашем примере слово export применено к шаблону класса Queue в файле Queue.h; этот файл включен в файл Queue.C, содержащий определения функций-членов add() и remove()
    , которые автоматически становятся экспортируемыми и не должны присутствовать в других файлах перед конкретизацией.
    Отметим, что, хотя шаблон класса объявлен экспортируемым, его собственное определение должно присутствовать в файле
    User.C
    Конкретизация
    Queue::add()
    в User.C вводит определение класса, в котором объявлены функции-члены Queue::add() и Queue::remove(). Эти объявления обязаны предшествовать вызову указанных функций. Таким образом, слово export влияет лишь на обработку функций-членов и статических данных-членов.
    Экспортируемыми можно объявлять также отдельные члены шаблона. В этом случае ключевое слово export указывается не перед шаблоном класса, а только перед экспортируемыми членами. Например, если автор шаблона класса Queue хочет экспортировать лишь функцию-член Queue::add() (т.е. изъять из заголовочного файла Queue.h только ее определение), то слово export можно указать именно в определении функции-члена add():
    // ---- User.C ----
    #include "Queue.h" int main() {
    // конкретизация Queue
    Queue *p_qi = new Queue; int ival;
    // ...
    // правильно: конкретизация Queue::add( const int & ) p_qi->add( ival );
    // ... export template

    С++ для начинающих
    819
    Type& Queue::remove() { ... } void Queue::add( const Type &val ) { ... }
    Обратите внимание, что определение шаблона для функции-члена remove() перенесено в заголовочный файл Queue.h. Это необходимо, поскольку remove() более не находится в экспортируемом шаблоне и, следовательно, ее определение должно быть видно во всех файлах, где вызываются конкретизированные экземпляры.
    Определение функции-члена или статического члена шаблона объявляется экспортируемым только один раз во всей программе. Поскольку компилятор обрабатывает файлы последовательно, он обычно не в состоянии определить, что эти члены объявлены экспортируемыми в нескольких исходных файлах. В таком случае результаты могут быть следующими:

    при редактировании связей возникает ошибка, показывающая, что один и тот же член шаблона класса определен несколько раз;

    компилятор неоднократно конкретизирует некоторый член одним и тем же множеством аргументов шаблона, что приводит к ошибке повторного определения во время связывания программы;

    компилятор конкретизирует член с помощью одного из экспортированных определений шаблона, игнорируя все остальные.
    Следовательно, нельзя утверждать, что при наличии в программе нескольких определений экспортированного члена шаблона обязательно будет сгенерирована ошибка. Создавая программу, надо быть внимательным и следить за тем, чтобы определения членов находились только в одном исходном файле.
    Модель с разделением позволяет отделить интерфейс шаблона класса от его реализации и организовать программу так, что эти интерфейсы помещаются в заголовочные файлы, а реализации – в файлы с исходным текстом. Однако не все компиляторы поддерживают данную модель, а те, которые поддерживают, не всегда делают это правильно: для этого требуется более изощренная среда программирования, которая доступна не во всех реализациях C++.
    // ---- Queue.h ---- template class Queue {
    // ... public:
    Type& remove(); void add( const Type & );
    // ...
    };
    // необходимо, так как remove() не экспортируется template
    // ---- Queue.C ----
    #include "Queue.h"
    // экспортируется только функция-член add() export template

    С++ для начинающих
    820
    В нашей книге используется только модель с включением, так как примеры работы с шаблонами небольшие и хотелось, чтобы они компилировались максимально большим числом компиляторов.
    16.8.3.
    Явные объявления конкретизации
    При использовании модели с включением определение члена шаблона класса помещается в каждый исходный файл, где может употребляться конкретизированный экземпляр.
    Точно неизвестно, где и когда компилятор конкретизирует такое определение, и некоторые компиляторы (особенно более старые) конкретизируют определение члена данным множеством аргументов шаблона неоднократно. Для использования в программе
    (на этапе сборки или на одной из предшествующих ей стадий) выбирается один из полученных экземпляров, а остальные игнорируются.
    Результат работы программы не зависит от того, сколько раз конкретизировался шаблон: в конечном итоге употребляется лишь один экземпляр. Однако, если приложение состоит из большого числа файлов и некоторый шаблон конкретизируется в каждом из них, то время компиляции заметно возрастает.
    Подобные проблемы, характерные для старых компиляторов, затрудняли использование шаблонов. Чтобы помочь программисту управлять моментом, когда конкретизация происходит, в стандарте C++ введено понятие явного объявления конкретизации, где за ключевым словом template идет слово class и имя конкретизируемого шаблона класса.
    В следующем примере явно объявляется конкретизация шаблона Queue, в котором запрашивается конкретизация аргументом int шаблона класса Queue: template class Queue;
    Если шаблон класса конкретизируется явно, то явно конкретизируются и все его члены, причем тем же типом аргумента. Следовательно, в файле, где встречается явное объявление, должно присутствовать не только определение шаблона, но и определения всех его членов. В противном случае выдается сообщение об ошибке: template class Queue;
    Если в некотором исходном файле встречается явное объявление конкретизации, то что произойдет в других файлах, где используется такой же конкретизированный шаблон?
    Как сказать компилятору, что явное объявление имеется в другом файле и что при употреблении шаблона класса или его членов в этом файле конкретизировать ничего не надо?
    Здесь, как и при использовании шаблонов функций (см. раздел 10.5.3), необходимо применить опцию компилятора, подавляющую неявные конкретизации. Эта опция
    #include "Queue.h"
    // явное объявление конкретизации template class Queue;
    // ошибка: шаблон Queue и его члены не определены

    С++ для начинающих
    821
    вынуждает компилятор предполагать, что все конкретизации шаблонов будут объявляться явно.
    Упражнение 16.9
    Куда бы вы поместили определения функций-членов и статических данных-членов своих шаблонов классов, если имеющийся у вас компилятор поддерживает модель компиляции с разделением? Объясните почему.
    Упражнение 16.10
    Имеется шаблон класса Screen, разработанный в упражнениях из предыдущих разделов
    (в том числе функции-члены, определенные в упражнении 16.5 из раздела 16.3, и статические члены, определенные в упражнении 16.7 из раздела 16.5). Организуйте программу так, чтобы воспользоваться преимуществами модели компиляции с разделением.
    16.9.
    Специализации шаблонов классов A
    Прежде чем приступать к рассмотрению специализаций шаблонов классов и причин, по которым в них может возникнуть надобность, добавим в шаблон Queue функции-члены min()
    и max(). Они будут обходить все элементы очереди и искать среди них соответственно минимальное и максимальное значения (правильнее, конечно, использовать для этой цели обобщенные алгоритмы min() и max(), представленные в главе 12, но мы определим эти функции как члены шаблона Queue, чтобы познакомиться со специализациями.) template class Queue {
    // ... public:
    Type min();
    Type max();
    // ...
    };
    // найти минимальное значение в очереди Queue template
    Type Queue::min()
    { assert( ! is_empty() );
    Type min_val = front->item; for ( QueueItem *pq = front->next; pq != 0; pq = pq->next ) if ( pq->item < min_val ) min_val = pq->item; return min_val;
    }
    // найти максимальное значение в очереди Queue template
    Type Queue::max()
    { assert( ! is_empty() );
    Type max_val = front->item; for ( QueueItem *pq = front->next; pq != 0; pq = pq->next ) if ( pq->item > max_val ) max_val = pq->item; return max_val;

    С++ для начинающих
    822
    }
    Следующая инструкция в функции-члене min() сравнивает два элемента очереди Queue: pq->item < min_val
    Здесь неявно присутствует требование к типам, которыми может конкретизироваться шаблон класса Queue: такой тип должен либо иметь возможность пользоваться предопределенным оператором “меньше” для встроенных типов, либо быть классом, в котором определен оператор operator<(). Если же этого оператора нет, то попытка применить min() к очереди приведет к ошибке компиляции в том месте, где вызывается несуществующий оператор сравнения. (Аналогичная проблема существует и в max(), только касается оператора operator>()).
    Предположим, что шаблон класса Queue нужно конкретизировать таким типом:
    };
    Но в этом классе нет оператора operator<(), позволяющего сравнивать два значения типа LongDouble, поэтому использовать для очереди типа Queue функции- члены min() и max() нельзя. Одним из решений этой проблемы может стать определение глобальных operator<() и operator>(), в которых для сравнения значений типа
    Queue
    используется функция-член compareLess. Эти глобальные операторы вызывались бы из min() и max() автоматически при сравнении объектов из очереди.
    Однако мы рассмотрим другое решение, связанное со специализацией шаблонов класса: вместо общих определений функций-членов min() и max() при конкретизации шаблона
    Queue типом
    LongDouble мы определим специальные экземпляры
    Queue::min()
    и Queue::max(), основанные на функции- члене compareLess() класса LongDouble.
    Это можно сделать, если воспользоваться явным определением специализации, где после ключевого слова template идет пара угловых скобок <>, а за ней – определение специализации члена класса. В приведенном примере для функций-членов min() и max() класса Queue, конкретизированного из шаблона, определены явные специализации: class LongSouble { public:
    LongDouble( double dbval ) : value( dval ) { } bool compareLess( const LongDouble & ); private: double value;

    С++ для начинающих
    823
    }
    Хотя тип класса Queue конкретизируется по шаблону, в каждом объекте этого типа используются специализированные функции-члены min() и max() – не те, что конкретизируются по обобщенным определениям этих функций в шаблоне класса Queue.
    Поскольку определения явных специализаций min() и max() – это определения невстроенных функций, помещать их в заголовочный файл нельзя: они обязаны находится в файле с текстом программы. Однако явную специализацию функции можно объявить, не определяя. Например: template <> LongDouble Queue::max();
    Поместив эти объявления в заголовочный файл, а соответствующие определения – в исходный, мы можем организовать код так же, как и для определений функций-членов обычного класса.
    Иногда определение всего шаблона оказывается непригодным для конкретизации некоторым типом. В таком случае программист может специализировать шаблон класса целиком. Напишем полное определение класса Queue:
    // определения явных специализаций template<> LongDouble Queue::min()
    { assert( ! is_empty() );
    LongDouble min_val = front->item; for ( QueueItem *pq = front->next; pq != 0; pq = pq->next ) if ( pq->item.compareLess( min_val ) ) min_val = pq->item; return min_val;
    } template<> LongDouble Queue::max()
    { assert( ! is_empty() );
    LongDouble max_val = front->item; for ( QueueItem *pq = front->next; pq != 0; pq = pq->next ) if ( max_val.compareLess( pq->item ) ) max_val = pq->item; return max_val;
    // объявления явных специализаций функций-членов template <> LongDouble Queue::min();

    С++ для начинающих
    824
    };
    Явную специализацию шаблона класса можно определять только после того, как общий шаблон уже был объявлен (хотя и не обязательно определен). Иными словами, должно быть известно, что специализируемое имя обозначает шаблон класса. Если в приведенном примере не включить заголовочный файл Queue.h перед определением явной специализации шаблона, компилятор выдаст сообщение об ошибке, указывая, что
    Queue
    – это не имя шаблона.
    Если мы определяем специализацию всего шаблона класса, то должны определить также все без исключения функции-члены и статические данные-члены. Определения членов из общего шаблона никогда не используются для создания определений членов явной специализации: множества членов этих шаблонов могут различаться. Чтобы предоставить определение явной специализации для типа класса Queue, придется определить не только функции-члены min() и max(), но и все остальные.
    Если класс специализируется целиком, лексемы template<> помещаются только перед определением явной специализации всего шаблона:
    LongDouble Queue::min() { }
    Класс не может в одних файлах конкретизироваться из общего определения шаблона, а в других – из специализированного, если задано одно и то же множество аргументов.
    Например, специализацию шаблона QueueItem необходимо объявлять в каждом файле, где она используется:
    // QueueLD.h: определяет специализацию класса Queue
    #include "Queue.h" template<> Queue {
    Queue();
    Queue();
    LongDouble& remove(); void add( const LongDouble & ); bool is_empty() const;
    LongDouble min();
    LongDouble max(); private:
    //
    Некоторая реализация
    #include "QueueLD.h"
    // определяет функцию-член min()
    // из специализированного шаблона класса
    // ---- File1.C ----
    #include "Queue.h" void ReadIn( Queue *pq ) {
    // использование pq->add()
    // приводит к конкретизации QueueItem
    }

    С++ для начинающих
    825
    }
    Эта программа некорректна, хотя большинство компиляторов ошибку не обнаружат: заголовочный файл QueueLD.h следует включать во все файлы, где используется
    Queue
    , причем до первого использования.
    16.10.
    Частичные специализации шаблонов классов A
    Если у шаблона класса есть несколько параметров, то можно специализировать его только для одного или нескольких аргументов, оставляя другие неспециализированными.
    Иными словами, допустимо написать шаблон, соответствующий общему во всем, кроме тех параметров, вместо которых подставлены фактические типы или значения. Такой механизм носит название частичной специализации шаблона класса. Она может понадобиться при определении реализации, более подходящей для конкретного набора аргументов.
    Рассмотрим шаблон класса Screen, введенный в разделе 16.2. Частичная специализации
    Screen
    1   ...   72   73   74   75   76   77   78   79   ...   93


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