Рассмотрим еще один пример шаблона - функцию сложения двух операндов:
template < typename T >
T add(T a, T b)
{
return a + b;
}
Отличие этого примера от abs в том, что предполагается передача двух аргументов одного и того же типа T. Если не указывать аргумент шаблона в явном виде, не действуют никакие правила преобразования типов, как для обычных функций. Компилятор позволяет в таком случае только однозначное соответствие во избежание недоразумений. Первые два примера компилируются, так как оба операнда имеют одинаковый однозначный тип, а последний - не компилируется, поскольку тип T из такого контекста неоднозначен:
int main ()
{
int x = add(2, 3); // ОК: add< int >
double y = add(2.5, 3.5); // ОК: add< double >
double z = add(2, 2.5); // Ошибка
}
error C2782: 'T add(T,T)': template parameter 'T' is ambiguous
see declaration of 'add'
could be 'double'
or 'int'
Проблему можно решить явным указанием типа либо преобразованием фактического аргумента:
double z1 = add< int >(2, 2.5); // ОК: add< int >, округление 2-го операнда
double z2 = add< double >(2, 2.5); // ОК: add< double >
double z3 = add((double) 2, 2.5); // ОК: add< double >
double z4 = add(2, (int) 2.5); // ОК: add< int >
Чтобы разрешить работу с несколькими типами сразу, шаблоны могут содержать несколько аргументов. Например:
template < typename RT, typename T1, typename T2 >
RT add (T1 a, T2 b)
{
return a + b;
}
Для явного указания аргументов такого шаблона при вызове следует перечислить фактические типы в угловых скобках через запятую:
short x = 20000;
int result = add< int, char, short >(‘a’, x);
Автоматический вывод типов может быть легко получен для передаваемых аргументов:
short x = 20000;
int result = add< int >(‘a’, x);
До С++’11 в подобных задачах тип результата приходилось указывать в явном виде в любом случае. Еще одну хитрость можно произвести на основе новейшей конструкции языка - операторе decltype. Такой оператор можно применить к любому выражению и использовать в контекстах, в которых обычно ожидается тип данных. Например:
decltype (2.5) x;
создаст переменную x с типом double. В обычном коде применение такого оператора значительно снижает читабельность и является ничем не оправданным. Однако в коде шаблонов такой оператор может найти свое разумное применение, если в качестве выражений подавать конструкции, тип которых не известен без фактического инстанцирования. В решаемой задаче сложения при помощи оператора decltype можно записать тип для выражения суммы двух аргументов:
template < typename T1, typename T2 >
decltype (T1() + T2()) add (T1 a, T2 b)
{
return a + b;
}
Разберем данное выражение подробнее:
decltype (T1() + T2())
Во время компиляции (не во время выполнения!) создаются значения по умолчанию для типа T1 и типа T2. Для числовых типов это приведет к созданию нулевых значений, но соответствующих типов. Далее формируется выражение сложения. Его значение никого не интересует, зато компилятор может при помощи оператора decltype автоматически вывести его тип, в соответствии с правилами языка. В итоге, функция add получает правильный возвращаемый тип без явного указания. Наконец, правильно и без лишнего синтаксического мусора будет работать такой клиентский код:
short x = 20000;
int result = add(‘a’, x);
Использование оператора decltype в качестве возвращаемого типа функций визуально нравится далеко не всем, поскольку трудно воспринять границу между возвращаемым типом и названием функции. В связи с этим, в новейшем стандарте С++14 была предложена альтернативная запись, подчеркивающая автоматический вывод возвращаемого типа, в которой выражение на основе decltype указывается не в начале объявления, а в конце после аргументов:
template < typename T1, typename T2 >
auto add (T1 a, T2 b) -> decltype (T1() + T2())
{
return a + b;
}
Какую форму записи выбрать - дело вкуса программиста. Поведение обеих форм эквивалентно.
Шаблоны классов
Аналогично функциям, классы также можно параметризовать относительно одного или нескольких типов-аргументов. При помощи шаблонов классов удобно реализуются универсальные структуры данных. Как и в шаблоне функции, объявлению класса должна предшествовать часть template < typename T> со списком аргументов. Аргументов также может быть несколько.
При инстанцировании шаблона класса компилятор, также как и с функциями, создает копию его определения с подставленными фактическими типами. Также инстанцируются тела только тех методов, которые реально вызываются в коде. Интересной особенностью схемы компиляции является тот факт, что если метод конкретного экземпляра шаблона класса не вызывается ни кем в коде, то компилятор даже не пытается инстанцировать такой метод. Отсюда вытекает негласное правило, что при разработке шаблонов очень важно инстанцировать весь написанный код в целях простейшего тестирования, поскольку без реального вызова функции - ее тело не будет никем проверяться и может содержать невыявленные ошибки!
#include <iostream>
template < typename T>
class Test
{
public:
void f (T x)
{
// Вообще-то, не факт, что переменную типа T можно разыменовать!
* x = 5;
}
void g ()
{
std::cout << "Saying hello!" << std::endl;
}
};
int main ()
{
// Создаем экземпляр шаблона класса с типом int.
// Разыменовывать тип int, как требует функция f, нельзя,
// но все прекрасно работает, потому что мы не вызываем функцию f!
Test< int > t;
t.g();
}
Каждый инстанцированный вариант шаблона класса - это отдельный класс. Несмотря на порождение от одного и того же источника, типы Test<int> и Test<short> - это разные классы, их нельзя приравнивать друг другу.
Также из этого вытекает, что у каждого из экземпляров будут свои наборы статических членов. Предположим, в шаблоне класса имеется статический член, подсчитывающий количество объектов. Статические переменные-члены класса Test<int> не имеют ничего общего со статическими членами класса Test<short>, и потому счетчики нужно инициализировать в глобальной области отдельно, и манипулировать ими отдельно в дальнейшем:
#include <iostream>
template < typename T >
class Test
{
public:
static int ms_objectCounter;
public:
Test () { ++ ms_objectCounter; }
Test (const Test< T > & _t) { ++ ms_objectCounter; }
};
int Test< int >::ms_objectCounter;
int Test< short >::ms_objectCounter;
int main ()
{
Test< int > ti1;
Test< int > ti2 = ti1;
std::cout << Test< int >::ms_objectCounter << std::endl;
std::cout << Test< short >::ms_objectCounter << std::endl;
}
Аргументы шаблонов классов могут иметь типы по умолчанию, если какой-либо из типов используется чаще других:
template < typename T = int >
class Test
{
//...
};
До появления стандарта С++’11 иметь значения по умолчанию разрешалось только аргументам шаблонов классов, но не функций. В новой редакции это ограничение для функций было снято.
Ниже приведен полный пример полезного класса-шаблона для обобщенного АТД “стек” фиксированного размера. Отметим несколько основных правил написания шаблонов классов:
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);
stack.hpp
#ifndef _STACK_HPP_
#define _STACK_HPP_
#include <stdexcept>
#include <initializer_list>
//*****************************************************************************
template < typename T >
class Stack
{
/*-----------------------------------------------------------------*/
public:
/*-----------------------------------------------------------------*/
// Конструктор
Stack (int _size = 10);
// Конструктор по списку инициализаторов
Stack (std::initializer_list< T > _l);
// Конструктор копий
Stack (const Stack< T > & _s);
// Конструктор перемещения
Stack (Stack< T > && _s);
// Деструктор
~Stack ();
// Оператор копирующего присвоения
Stack< T > & operator = (const Stack< T >& _s);
// Оператор перемещающего присвоения
Stack< T > & operator = (Stack< T > && _s);
// Метод добавления значения в стек
void push (const T& _value);
// Метод удаления значения с вершины стека
void pop ();
// Метод доступа к значению на вершине стека
T & top () const;
// Метод определения пустоты стека
bool isEmpty () const;
// Метод определения заполненности стека
bool isFull () const;
/*-----------------------------------------------------------------*/
private:
/*-----------------------------------------------------------------*/
// Размер стека
int m_size;
// Указатель на начало блока данных
T* m_pData;
// Указатель на вершину стека
T* m_pTop;
/*-----------------------------------------------------------------*/
};
//*****************************************************************************
// Реализация конструктора
template < typename T >
Stack< T >::Stack (int _size)
: m_size(_size)
{
// Проверка корректности размера стека
if (m_size <= 0)
throw std::logic_error("Non-positive size");
// Выделяем массив для хранения данных стека
m_pData = new T[ m_size ];
// Устанавливаем вершину в позицию начала блока данных
m_pTop = m_pData;
}
//*****************************************************************************
// Реализация конструктора по списку инициализаторов
template < typename T >
Stack< T >::Stack (std::initializer_list< T > _l)
: Stack(_l.size())
{
// Поэлементное копирование содержимого списка инициализаторов
for (const T & x: _l)
push(x);
}
//*****************************************************************************
// Реализация конструктора копий
template < typename T >
Stack< T >::Stack (const Stack< T >& _s)
: m_size(_s.m_size)
{
// Выделяем массив для хранения данных стека
m_pData = new T[ m_size ];
m_pTop = m_pData;
// Поочередно вставлем элементы
int nActual = _s.m_pTop - _s.m_pData;
for (int i = 0; i < nActual; i++)
push(_s.m_pData[ i ]);
}
//*****************************************************************************
// Реализация конструктора перемещения
template < typename T >
Stack< T >::Stack (Stack< T > && _s)
: m_size(_s.m_size),
m_pData(_s.m_pData),
m_pTop(_s.m_pTop)
{
// Отбираем ресурсы у “умирающего” другого стека
_s.m_pData = _s.m_pTop = nullptr;
}
//*****************************************************************************
// Реализация деструктора
template < typename T >
Stack< T >::~Stack ()
{
delete[] m_pData;
}
//*****************************************************************************
// Реализация оператора копирующего присвоения
template < typename T >
Stack< T >& Stack< T >:: operator = (const Stack< T >& _s)
{
// Защита от присвоения на самого себя
if (this == & _s)
return * this;
// Освобождаем старый блок и выделяем новый
delete[] m_pData;
m_size = _s.m_size;
m_pData = new T[ m_size ];
// Копируем полезные данные из другого стека
int nActual = _s.m_pTop - _s.m_pData;
for (int i = 0; i < nActual; i++)
m_pData[ i ] = _s.m_pData[ i ];
// Выставляем вершину стека в аналогичную другому стеку позицию
m_pTop = m_pData + nActual;
// Возвращаем ссылку на себя
return * this;
}
//*****************************************************************************
// Реализация оператора перемещающего присвоения
template < typename T >
Stack< T >& Stack< T >:: operator = (Stack< T > && _s)
{
// Защита от присвоения на самого себя
if (this == & _s)
return * this;
// Освобождаем старый блок данных
delete[] m_pData;
// Присваиваем себе ресурсы другого “умирающего” стека
m_size = _s.m_size;
m_pData = _s.m_pData;
m_pTop = _s.m_pTop;
// Отцепляем ресурсы от другого стека
_s.m_pData = _s.m_pTop = nullptr;
// Возвращаем ссылку на себя
return * this;
}
//*****************************************************************************
// Реализация метода добавления значения в стек
template < typename T>
void Stack< T >::push (const T& _value)
{
// Стек не должен быть заполнен на 100% в данный момент
if (isFull())
throw std::logic_error("Stack overflow error");
// Размещаем новое значение в стеке и увеличиваем указатель-вершину
* m_pTop++ = _value;
}
//*****************************************************************************
// Реализация метода удаления значения с вершины стека
template < typename T >
void Stack< T >::pop ()
{
// Стек не должен быть пустым в данный момент
if (isEmpty())
throw std::logic_error("Stack underflow error");
// Уменьшаем указатель-вершину
m_pTop--;
}
//*****************************************************************************
// Реализация метода доступа к значению на вершине стека
template < typename T >
T& Stack< T >::top () const
{
// Стек не должен быть пустым в данный момент
if (isEmpty())
throw std::logic_error("Stack is empty");
// Возвращаем ссылку на значение, находящееся под указателем-вершиной
return *(m_pTop - 1);
}
//*****************************************************************************
// Реализация метода определения пустоты стека
template < typename T >