Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ООП_ Лекция №06 - Шаблоны. Основы обобщенного программирования..docx
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
138.46 Кб
Скачать

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

Аналогично функциям, классы также можно параметризовать относительно одного или нескольких типов-аргументов. При помощи шаблонов классов удобно реализуются универсальные структуры данных. Как и в шаблоне функции, объявлению класса должна предшествовать часть 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 );

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

template< typename T >

Stack< T >::Stack ( int _size )

: m_size( _size )

{

m_pData = new T[ m_size ];

m_pTop = m_pData;

}

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

  1. Пока не известен конкретный тип аргумента шаблона, ничего нельзя утверждать о размере этого объекта. Возникает вопрос способа передачи обобщенных значений в методы стека - по значению или по ссылке? Во избежание избыточных копирований для больших объектов обычно в обобщенном коде передают ссылки, надеясь что производительность передачи ссылки для маленьких объектов (например, 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.