Шаблоны функций и классов. Синтаксис определения шаблонов. Инстанцирование шаблонов. Модель включения и явное инстанцирование

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

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

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

template< typenameT >

Tabs ( T _value )

{

return( _value < 0 ) ? - value : value;

}

Ключевое слово template означает, что далее записывается шаблон. В угловых скобках указываются аргументы шаблона - декларация typenameT означает, что описание будет производиться относительно неизвестного на данном этапе типа данных T. В дальнейшем это имя может использоваться как в прототипе так и в теле шаблона.

В состав шаблонов функций входит два наборами аргументов:

● аргументы шаблона (typename T);

● аргументы функции (T _value).

Аргументы шаблона используются для подстановки конкретных типов, а аргументы функции - в обычных целях, для передачи данных.

Вместо ключевого слова typename можно использовать слово class. С точки зрения поведения в данном контексте эти ключевые слова играют идентичную роль. Ключевое слово typename является предпочтительным из стилистических соображений, поскольку по смыслу вместо аргумента T могут подставляться не только классы, но и встроенные типы, такие как int. Учитывая возможность подстановки встроенных типов, ключевое слово class может сбивать с толку, хотя и не окажет никакого влияния на функциональность.

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

Шаблон функции еще не является полноценной функцией, это лишь некоторая промежуточная форма в памяти компилятора. Если шаблон не использовать, никакого машинного кода не будет сгенерировано вообще!

Второй этап компиляции шаблона выполняется при подстановке конкретных типов вместо аргументов. Его называют инстанцированием (instantiation) шаблона. Фактический аргумент для типа T можно указать явно:

std::cout << abs< double>( -2.5 );

std::cout << abs< int>( -2 );

std::cout << abs< Money>( Money( -5, 20 ) ).m_dollars;

либо для простых случаев он будет автоматически выведен (deducted) из контекста по переданным аргументам:

std::cout << abs( -2.5 );

std::cout << abs( -2 );

std::cout << abs( Money( -5, 20 ) ).m_dollars;

Для каждого использования шаблона с уникальным фактическим аргументом компилятор в результате инстанцирования сгенерирует отдельную копию с подставленными вместо T конкретным типом. При этом выполнятся все ранее отложенные компилятором проверки, ожидавшие информации о конкретном типе T.

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

Для явного указания аргументов такого шаблона при вызове следует перечислить фактические типы в угловых скобках через запятую:

shortx = 20000;

intresult = add< int, char, short>( ‘a’, x);

Автоматический вывод типов может быть получен лишь для передаваемых аргументов:

shortx = 20000;

intresult = add< int>( ‘a’, x );

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

Шаблоны классов

Аналогично функциям, классы также можно параметризовать относительно одного или нескольких типов. При помощи шаблонов классов удобно реализуются универсальные структуры данных. Как и в шаблоне функции, объявлению класса должна предшествовать часть template< typenameT> со списком аргументов. Аргументов также может быть несколько.

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

#include<iostream>

template< typenameT>

classTest

{

public:

voidf ( T x )

{

// Вообще-то, не факт, что переменную типа T можно разыменовать!

* x = 5;

}

voidg ()

{

std::cout << "Saying hello!" << std::endl;

}
};

intmain ()

{

// Создаем экземпляр шаблона класса с типом int.

// Разыменовывать тип int, как требует функция f, нельзя,

// но все прекрасно работает, потому что мы не вызываем функцию f!

Test< int> t;

t.g();
}

Каждый инстанцированный вариант шаблона класса - это отдельный класс. Несмотря на порождение от одного и того же источника, типы Test<int> и Test<short> - это разные классы, их нельзя приравнивать друг другу.

Также из этого вытекает, что у каждого из экземпляров будут свои наборы статических членов. Предположим, в шаблоне класса имеется статический член, подсчитывающий количество объектов. Статические переменные-члены класса Test<int> не имеют ничего общего со статическими членами класса Test<short>, и потому счетчики нужно инициализировать в глобальной области отдельно, и манипулировать ими отдельно в дальнейшем:

#include<iostream>

template< typenameT >

classTest

{

public:

static intms_objectCounter;

public:

Test () { ++ ms_objectCounter; }

Test ( constTest< T > & _t ) { ++ ms_objectCounter; }

};

intTest< int>::ms_objectCounter;

intTest< short>::ms_objectCounter;

intmain ()

{

Test< int> ti1;

Test< int> ti2 = ti1;

std::cout << Test< int>::ms_objectCounter << std::endl;

std::cout << Test< short>::ms_objectCounter << std::endl;

}

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

template< typenameT = int>

classTest

{

// ...

};

Ниже приведен полный пример полезного класса-шаблона для обобщенного АТД “стек” фиксированного размера. Отметим несколько основных правил написания шаблонов классов:

1. При определении шаблона класса может возникнуть путаница с использованием его имени внутри определения. Когда контекст требует использовать имя класса, например, чтобы задать конструктор, оно указывается как обычно:

// Конструктор

Stack ( int_size = 10 );

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

// Оператор копирующего присвоения

Stack< T > & operator= ( constStack< T >& _s );

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

template< typenameT >

Stack< T >::Stack ( int_size )

: m_size( _size )

{

m_pData = newT[ m_size ];

m_pTop = m_pData;

}

3. Чаще всего тела методов шаблонов классов размещают непосредственно в заголовочном файле после объявления класса. Это работает корректно, даже если функции не объявляются как встраиваемые (inline). CPP-файла для шаблона-класса чаще всего не создают вообще. Именно так выглядит практически весь код стандартной библиотеки шаблонов. Такой стиль реализации, не свойственный обычным классам C++, обуславливается особенностями компоновки шаблонов. Пока примем это как утверждение без объяснения, а детально разъясним позже.

4. Пока не известен конкретный тип аргумента шаблона, ничего нельзя утверждать о размере этого объекта. Возникает вопрос способа передачи обобщенных значений в методы стека - по значению или по ссылке? Во избежание избыточных копирований для больших объектов обычно в обобщенном коде передают ссылки, надеясь что производительность передачи ссылки для маленьких объектов (например, char) не слишком уступит передаче по значению:

voidpush ( constT& _value );

Альтернативой включению тел методов в заголовочные файлы является механизм явного инстанцирования (explicit instantiation). Если заранее известно с какими типами будет инстанцирован шаблон класса, то можно заранее искусственно инстанцировать тела всех методов в CPP-файле реализации. Например, известно ограничение, что шаблон MyTemplate будет инстанцироваться только с двумя типами - int и std::string. В таком случае в конце файла mytemplate.cpp следует добавить следующие директивы явного инстанцирования:

template classMyTemplate< int>;

template classMyTemplate< std::string >;

Увидев такие директивы, компилятор инстанцирует все известные ему тела методов шаблона MyTemplate с указанными типами, будет сгенерирован реальный машинный код, который попадет в объектный файл. В последствии, при использовании данного шаблона в другом CPP-файле, компоновщик без труда обнаружит подходящие функции.

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

Наши рекомендации