- •Ооп: Лекция 6. Шаблоны. Основы обобщенного программирования.
- •Введение в шаблоны - шаблоны функций
- •Автоматический вывод типов в шаблонах
- •Шаблоны классов
- •Технические проблемы использования шаблонов
- •Константы в аргументах шаблона
- •Компоновка шаблонов
- •Пример: собственная шаблонная реализация std::initializer_list
- •Пример: шаблонная реализация связного списка
- •Обобщенные алгоритмы
- •Полные примеры из лекции
Шаблоны классов
Аналогично функциям, классы также можно параметризовать относительно одного или нескольких типов-аргументов. При помощи шаблонов классов удобно реализуются универсальные структуры данных. Как и в шаблоне функции, объявлению класса должна предшествовать часть 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 иметь значения по умолчанию разрешалось только аргументам шаблонов классов, но не функций. В новой редакции это ограничение для функций было снято.
Ниже приведен полный пример полезного класса-шаблона для обобщенного АТД “стек” фиксированного размера. Отметим несколько основных правил написания шаблонов классов:
При определении шаблона класса может возникнуть путаница с использованием его имени внутри определения. Когда контекст требует использовать имя класса, например, чтобы задать конструктор, оно указывается как обычно:
// Конструктор
Stack ( int _size = 10 );
Когда же речь идет о классе как о типе, рекомендуется явно указывать его в обобщенном виде с указанием аргумента шаблона:
// Оператор копирующего присвоения
Stack< T > & operator = ( const Stack< T >& _s );
Как и для обычного класса, реализация методов шаблона класса может находиться как внутри объявления класса, так и за его пределами. Если размещать реализацию методов отдельно от определения класса, то нужно использовать следующий синтаксис:
template< typename T >
Stack< T >::Stack ( int _size )
: m_size( _size )
{
m_pData = new T[ m_size ];
m_pTop = m_pData;
}
Чаще всего тела методов шаблонов классов размещают непосредственно в заголовочном файле после объявления класса. Это работает корректно, даже если функции не объявляются как встраиваемые (inline). CPP-файла для шаблона-класса чаще всего не создают вообще. Именно так выглядит практически весь код стандартной библиотеки шаблонов. Такой стиль реализации, не свойственный обычным классам C++, обуславливается особенностями компоновки шаблонов. Пока примем это как утверждение без объяснения, а детально разъясним позже.
Пока не известен конкретный тип аргумента шаблона, ничего нельзя утверждать о размере этого объекта. Возникает вопрос способа передачи обобщенных значений в методы стека - по значению или по ссылке? Во избежание избыточных копирований для больших объектов обычно в обобщенном коде передают ссылки, надеясь что производительность передачи ссылки для маленьких объектов (например, 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 >
bool Stack< T >::isEmpty () const
{
return m_pTop == m_pData;
}
//*****************************************************************************
// Реализация метода определения заполненности стека
template< typename T >
bool Stack< T >::isFull () const
{
return ( m_pTop - m_pData ) == m_size;
}
//*****************************************************************************
#endif // _STACK_HPP_
Создадим тестовую программу на основе разработанного шаблона. Однажды записав обобщенную структуру данных, можно применять ее для практически любого типа. Требования, которые накладываются к типу T такой реализацией состоят в возможности копирования значения, присвоения, а также наличия конструктора по умолчанию (для выделения массива).
test_stack.cpp
//*****************************************************************************
#include "stack.hpp"
#include <string>
#include <cassert>
//*****************************************************************************
int main ()
{
// Стек целых чисел
Stack< int > s1;
s1.push( 5 );
assert( ! s1.isEmpty() && s1.top() == 5 );
// Стек действительных чисел
Stack< double > s2;
s2.push( 2.5 );
assert( ! s2.isEmpty() && s2.top() == 2.5 );
// Стек объектов-строк
Stack< std::string > s3;
s3.push( "Hello" );
assert( ! s3.isEmpty() && s3.top() == "Hello" );
// Даже стек стеков целых чисел!
Stack< Stack< int > > s4;
s4.push( Stack< int >() );
s4.top().push( 5 );
assert( ! s4.isEmpty() && !s4.top().isEmpty() && s4.top().top() == 5 );
}
//*****************************************************************************
В таком варианте реализации невозможно присвоение между экземплярами Stack< int > и Stack< double >, поскольку после инстанцирования они являются разными несвязанными классами. Однако для контейнеров значений такое поведение может быть весьма полезным на практике, разумеется, с преобразованием типа хранящихся значений. Чтобы разрешить такое копирование, нужно усовершенствовать конструктор копий, сделав его шаблоном-членом (member template). Аналогичный прием можно применить к конструктору, принимающему список инициализаторов, чтобы получить возможность создания стека на основе конвертируемых данных другого типа, например, создать стек целых чисел по массиву действительных.
В заголовочной части объявление конструктора копий и оператора копирующего присвоения следует видоизменить, а также объявить любой другой экземпляр того же класса другом для удобного доступа к его private-части:
template< typename T >
class Stack
{
//------------------------------------------------------------------------
public:
// ...
// Объявляем любой экземпляр Stack другом любого другого экземпляра Stack
template< typename > friend class Stack;
// Конструктор по обобщенному списку инициалиазторов
template< typename U >
Stack ( std::initializer_list< U > _l );
// Конструктор копий - шаблон-член, ожидает тип U, потенциально U != T
template< typename U >
Stack ( const Stack< U >& _s );
// ...
// Оператор копирующего присвоения - также шаблон-член
template< typename U >
Stack< T > & operator = ( const Stack< U >& _s );
// ...
};
Видоизмененная реализация будет выглядеть следующим образом:
template< typename T >
template< typename U > // Да, два раза template, это не ошибка!
Stack< T >::Stack ( std::initializer_list< U > _l )
: Stack( _l.size() )
{
// Перебираем список инициализаторов данных типа U
for ( const U & x : _l )
// Помещаем в стек данные типа T путем преобразования U к T
push( ( const T & ) x );
}
template< typename T >
template< typename U > // Да, два раза template, это не ошибка!
Stack< T >::Stack ( const Stack< U > & _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 ] ); // неявное преобразование типа от U к T
}
//*****************************************************************************
template< typename T >
template< typename U > // Да, два раза template, это не ошибка!
Stack< T >& Stack< T >::operator = ( const Stack< U >& _s )
{
// Защита от присвоения на самого себя - несколько усложняется,
// т.к. нельзя просто сравнивать адреса двух разных классов Stack<T> и Stack<U>!
if ( ( const void * )( this ) == ( const void * )( & _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 ]; // неявное преобразование типа от U к T
// Выставляем вершину стека в аналогичную другому стеку позицию
m_pTop = m_pData + nActual;
// Возвращаем ссылку на себя
return * this;
}
Теперь желаемое преобразование становится возможным:
test_stack_conversions.cpp
//*****************************************************************************
#include "stack.hpp"
#include <string>
#include <cassert>
//*****************************************************************************
int main ()
{
// Создаем стек целых чисел на основе массива действительных чисел
Stack< int > s = { 1.5, 2.5, 3.5 };
assert( ! s.isEmpty() && s.top() == 3 ); // 3.5 округлится до 3
// Стек действительных чисел
Stack< double > s1;
s1.push( 2.5 );
// Создаем стек целых чисел на основе стека действительных чисел!
Stack< int > s2 = s1;
assert( !s2.isEmpty() && s2.top() == 2 ); // 2.5 округлится до 2
// Стек строк в стиле C
Stack< const char * > s3;
s3.push( "Hello" );
// Присваиваем стеку строк std::string,
// неявно конструируя такие объекты из строкового литерала
Stack< std::string > s4;
s4 = s3;
assert( !s4.isEmpty() && s4.top() == "Hello" );
}
//*****************************************************************************
Следует отметить, что такой трюк не подходит для конструкторов перемещения и оператора перемещающего присвоения. Внутреннее представление стеков для разных типов-аргументов отличается, и нельзя просто “забрать” внутреннее представление другого не связанного объекта.
При необходимости, шаблоны-члены могут существовать в обычных классах:
#include <iostream>
class Test
{
public:
template< typename T >
void f ( const T & _val )
{
std::cout << _val << std::endl;
} };
int main ()
{
Test t;
t.f( 5 ); // вызов Test::f< int >
t.f( 2.5 ); // вызов Test::f< double > }
Каждый уникальный тип аргумент породит новый метод в классе Test.
