Аргументы шаблонов - типы, константы, шаблонные аргументы шаблонов. Дедукция фактических аргументов шаблонов
Аргументы шаблонов классов могут иметь типы по умолчанию, если какой-либо из типов используется чаще других:
template< typename T = int >
class Test
{
// ...
};
Ниже приведен полный пример полезного класса-шаблона для обобщенного АТД “стек” фиксированного размера. Отметим несколько основных правил написания шаблонов классов:
1. При определении шаблона класса может возникнуть путаница с использованием его имени внутри определения. Когда контекст требует использовать имя класса, например, чтобы задать конструктор, оно указывается как обычно:
// Конструктор
Stack ( int _size = 10 );
Когда же речь идет о классе как о типе, рекомендуется явно указывать его в обобщенном виде с указанием аргумента шаблона:
// Оператор копирующего присвоения
Stack< T > & operator = ( const Stack< T >& _s );
2. Как и для обычного класса, реализация методов шаблона класса может находиться как внутри объявления класса, так и за его пределами. Если размещать реализацию методов отдельно от определения класса, то нужно использовать следующий синтаксис:
template< typename T >
Stack< T >::Stack ( int _size )
: m_size( _size )
{
m_pData = new T[ m_size ];
m_pTop = m_pData;
}
3. Чаще всего тела методов шаблонов классов размещают непосредственно в заголовочном файле после объявления класса. Это работает корректно, даже если функции не объявляются как встраиваемые (inline). CPP-файла для шаблона-класса чаще всего не создают вообще. Именно так выглядит практически весь код стандартной библиотеки шаблонов. Такой стиль реализации, не свойственный обычным классам C++, обуславливается особенностями компоновки шаблонов. Пока примем это как утверждение без объяснения, а детально разъясним позже.
4. Пока не известен конкретный тип аргумента шаблона, ничего нельзя утверждать о размере этого объекта. Возникает вопрос способа передачи обобщенных значений в методы стека - по значению или по ссылке? Во избежание избыточных копирований для больших объектов обычно в обобщенном коде передают ссылки, надеясь что производительность передачи ссылки для маленьких объектов (например, char) не слишком уступит передаче по значению:
void push ( const T& _value );
Каждый уникальный тип аргумент породит новый метод в классе Test. Единственным ограничением является то, что шаблоном-членом не может быть виртуальная функция, поскольку компилятор обязан знать точное количество таких функций для формирования таблицы VTABLE и последующего переопределения в классах-наследниках.
Технические проблемы использования шаблонов
С разработкой шаблонов классов и функций связан ряд неприятных технических проблем. Написать компилирующийся и инстанцирующийся со всем желаемым набором типов обобщенный код является намного более сложной задачей, чем написание обычной необобщенной версии алгоритма или структуры данных.
Первое, с чем сталкиваются при работе с шаблонами - это сложные тексты ошибок компиляции, если что-либо записано не так. Нередко, вместо аккуратного простого описания о произошедшей ошибке, при компиляции шаблонного кода может быть выдана серия абсолютно непонятных ошибок сложной структуры, в которых не просто разобраться. В литературе такое поведение компиляторов называют “ошибками-романами”, сетуя на их количество и длину. В популярной книге о технике использования шаблонов “Шаблоны С++: справочник разработчика” приведен пример текста сгенерированной компилятором ошибки, вызванного совсем небольшой оплошностью в коде вызова шаблона функции, обрабатывающей связные списки строк. Длина сообщения действительно впечатляет, но это далеко на худший случай, который может встретиться на практике.
Второй проблемой являются имена генерируемых экземпляров функций и методов. На уровне реализации, имена включают все аргументы шаблонов, и могут становиться достаточно длинными. Длину идентификаторов также можно увидеть и оценить в приведенном выше примере. В более ранних версиях компиляторов и компоновщиков при интенсивном использовании шаблонов иногда случалась довольно редкая для обычного программного кода ошибка превышения допустимой длины идентификаторов.
Существует также ряд синтаксических нюансов, которые нужно соблюдать при написании кода с шаблонами. Например, некоторые более ранние компиляторы (например, Visual Studio до версии 2010) плохо реагируют на идущие подряд закрывающие угловые скобки вложенных шаблонов. Такой код может не скомпилироваться, поскольку компилятор может воспринять последовательность символов “>>” как оператор сдвига, а не как 2 идущие подряд закрывающие угловые скобки:
std::vector< std::vector< int >> vv;
Проблему решают вставкой дополнительного пробела:
std::vector< std::vector< int > > vv;
// ^ стоит поставить пробел
Еще одной надоедливой синтаксической проблемой является использование вложенных имен, зависящих от аргумента шаблона. Предположим, реализация предполагает, чтобы все аргументы T в приведенном ниже шаблоне имели вложенный синоним типа value_type:
template< typename T >
class Test
{
T::value_type x;
};
Существуют и более неприятные синтаксические странности. Например, в коде иногда может потребоваться объяснить некоторым компиляторам, что речь идет об аргументах шаблона, а не об операторе <, добавив странно выглядящее синтаксическое средство “. template”:
class Test
{
public:
template< typename T>
int f ( T _x );
};
template< typename T >
void f ( T _x )
{
Test t;
std::cout << t. template f< T >( _x );
// Оригинально: std::cout << t.f< T >( _x );
}
Еще одной проблемой практического применения шаблонов является разница в поддержке между компиляторами. Очень часто возникают ситуации, когда код прекрасно компилируется на одном компиляторе, но выдает ошибки на другом. Это связано с отступлением конкретными компиляторами от норм стандартов, а также с дефектами в реализации самих компиляторов. Чтобы получить действительно переносимый код, иногда потребуется вносить исправления после реализации и регулярно собирать программу на нескольких компиляторах.
Аргументы шаблонов, не являющиеся типами
Среди аргументов шаблонов могут быть не только типы. Допускается использование констант в качестве аргументов шаблонов. Например, размер стека можно было бы передавать не как аргумент конструктора, а задавать в списке аргументов шаблона, и тогда можно было бы обойтись без выделения динамической памяти, воспользовавшись обычным массивом:
template < typename T, int SIZE = 10 >
class Stack
{
private:
T data[ SIZE ];
int numElems;
public:
Stack();
void push( const T & );
void pop();
T & top() const;
bool empty() const;
bool full() const;
};
Существуют ограничения на допустимые типы констант - это должны быть целочисленные типы либо перечисления (в экзотических случаях допускаются также адреса глобальных объектов). Обычно такие константы используют для конфигурации режимов работы шаблона. Следует понимать, что любой код, использующий такие виды аргументов, видит конкретное значение константы при инстанцировании:
if ( SIZE > 10 )
do1();
else
do2();
Т.е, с точки зрения экземпляра шаблона, выражение SIZE > 10 является константным во время компиляции. Большинство компиляторов успешно оптимизирует код, содержащий константные выражения. Для приведенного выше примера в зависимости от значения SIZE будет выбрано первое либо второе действие, и никакой проверки условия во время выполнения происходить не будет. Результирующий фрагмент будет либо вызывать функцию do1(), либо функцию do2(), в зависимости от значения SIZE, с которым он будет инстанцирован.
Существует также особый вид аргументов шаблонов с непроизносимым названием - шаблонные параметры шаблонов (еще более непроизносимое на английском языке - template template parameters). Например, требуется реализовать АТД “очередь” на основе какого-либо контейнера. Чтобы позволить программисту выбирать между, например, двусвязным списком или деком (очередь с двумя концами), можно использовать шаблонный параметр шаблона:
template < typename T,
template < typename ELEM > class CONT = std::list >
class Queue
{
private:
CONT<T> elems;
public:
void push ( T const & _v )
{
elems.push_back( _v );
}
void pop ()
{
elems.pop_front();
}
T top () const
{
return elems.front();
}
bool empty () const
{
return elems.empty();
}
};
По умолчанию будет создаваться очередь на основе двусвязного списка элементов типа T:
Queue< T > q1; // Queue< T, std::list >
Чтобы изменить базовый контейнер, стоящий в основе реализации очереди, достаточно указать его явно:
Queue< T, std::deque > q1;
Такое переключение возможно, поскольку интерфейс std::list и std::deque преднамеренно хорошо согласован - основные открытые функции имеют одинаковые имена и подходящие типы аргументов. Разумеется, это не случайно, авторы стандартной библиотеке предусмотрели такое совпадение имен именно с целью облегчения написания обобщенного кода.