Автоматический вывод типов в шаблонах

 

Рассмотрим еще один пример шаблона - функцию сложения двух операндов:

 

template< typenameT >
T add( T a, T b )

{

returna + b;

}

 

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

 

intmain ()

{

intx = add( 2, 3 ); // ОК: add< int>

doubley = add( 2.5, 3.5 ); // ОК: add< double>

 

doublez = add( 2, 2.5 ); // Ошибка

}

 

error C2782: 'T add(T,T)' : template parameter 'T' is ambiguous

see declaration of 'add'

could be 'double'

or 'int'

 

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

 

doublez1 = add< int>( 2, 2.5 ); // ОК: add< int>, округление 2-го операнда

doublez2 = add< double>( 2, 2.5 ); // ОК: add< double>

 

doublez3 = add( ( double) 2, 2.5 ); // ОК: add< double>

doublez4 = add( 2, ( int) 2.5 ); // ОК: add< int>

 

Чтобы разрешить работу с несколькими типами сразу, шаблоны могут содержать несколько аргументов. Например:

 

template< typenameRT, typename T1, typenameT2 >
RT add ( T1 a, T2 b )

{

returna + b;

}

 

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

 

shortx = 20000;

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

 

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

 

shortx = 20000;

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

 

До С++’11 в подобных задачах тип результата приходилось указывать в явном виде в любом случае. Еще одну хитрость можно произвести на основе новейшей конструкции языка - операторе decltype. Такой оператор можно применить к любому выражению и использовать в контекстах, в которых обычно ожидается тип данных. Например:

 

decltype(2.5) x;

 

создаст переменную x с типом double. В обычном коде применение такого оператора значительно снижает читабельность и является ничем не оправданным. Однако в коде шаблонов такой оператор может найти свое разумное применение, если в качестве выражений подавать конструкции, тип которых не известен без фактического инстанцирования. В решаемой задаче сложения при помощи оператора decltype можно записать тип для выражения суммы двух аргументов:

 

template< typenameT1, typenameT2 >

decltype( T1() + T2() ) add ( T1 a, T2 b )

{

returna + b;

}

 

Разберем данное выражение подробнее:

 

decltype( T1() + T2() )

 

Во время компиляции (не во время выполнения!) создаются значения по умолчанию для типа T1 и типа T2. Для числовых типов это приведет к созданию нулевых значений, но соответствующих типов. Далее формируется выражение сложения. Его значение никого не интересует, зато компилятор может при помощи оператора decltype автоматически вывести его тип, в соответствии с правилами языка. В итоге, функция add получает правильный возвращаемый тип без явного указания. Наконец, правильно и без лишнего синтаксического мусора будет работать такой клиентский код:

 

shortx = 20000;

intresult = add( ‘a’, x );

 

Использование оператора decltype в качестве возвращаемого типа функций визуально нравится далеко не всем, поскольку трудно воспринять границу между возвращаемым типом и названием функции. В связи с этим, в новейшем стандарте С++14 была предложена альтернативная запись, подчеркивающая автоматический вывод возвращаемого типа, в которой выражение на основе decltype указывается не в начале объявления, а в конце после аргументов:

 

template< typenameT1, typenameT2 >

autoadd ( T1 a, T2 b ) -> decltype( T1() + T2() )

{

returna + b;

}

 

Какую форму записи выбрать - дело вкуса программиста. Поведение обеих форм эквивалентно.

 

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

 

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

{

// ...

};

 

До появления стандарта С++’11 иметь значения по умолчанию разрешалось только аргументам шаблонов классов, но не функций. В новой редакции это ограничение для функций было снято.

 

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

 

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 );

 

 

stack.hpp

 

#ifndef _STACK_HPP_

#define _STACK_HPP_

 

#include <stdexcept>

#include <initializer_list>

 

//*****************************************************************************

 

template< typenameT >

classStack

{

 

/*-----------------------------------------------------------------*/

 

public:

 

/*-----------------------------------------------------------------*/

 

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

Stack ( int_size = 10 );

 

// Конструктор по списку инициализаторов

Stack ( std::initializer_list< T > _l );

 

// Конструктор копий

Stack ( constStack< T > & _s );

 

// Конструктор перемещения

Stack ( Stack< T > && _s );

 

// Деструктор

~Stack ();

 

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

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

 

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

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

 

// Метод добавления значения в стек

voidpush ( constT& _value );

 

// Метод удаления значения с вершины стека

voidpop ();

 

// Метод доступа к значению на вершине стека

T & top () const;

 

// Метод определения пустоты стека

boolisEmpty () const;

 

// Метод определения заполненности стека

boolisFull () const;

 

/*-----------------------------------------------------------------*/

 

private:

 

/*-----------------------------------------------------------------*/

 

// Размер стека

intm_size;

 

// Указатель на начало блока данных

T* m_pData;

 

// Указатель на вершину стека

T* m_pTop;

 

/*-----------------------------------------------------------------*/

 

 

};

 

//*****************************************************************************

 

// Реализация конструктора

template< typenameT >

Stack< T >::Stack ( int_size )

: m_size( _size )

{

// Проверка корректности размера стека

if( m_size <= 0 )

throwstd::logic_error( "Non-positive size" );

 

// Выделяем массив для хранения данных стека

m_pData = newT[ m_size ];

 

// Устанавливаем вершину в позицию начала блока данных

m_pTop = m_pData;

}

 

//*****************************************************************************

 

// Реализация конструктора по списку инициализаторов

template< typenameT >

Stack< T >::Stack ( std::initializer_list< T > _l )

: Stack( _l.size() )

{

// Поэлементное копирование содержимого списка инициализаторов

for ( constT & x : _l )

push( x );

}

 

//*****************************************************************************

 

// Реализация конструктора копий

template< typenameT >

Stack< T >::Stack ( constStack< T >& _s )

: m_size( _s.m_size )

{

// Выделяем массив для хранения данных стека

m_pData = newT[ m_size ] ;

m_pTop = m_pData;

 

// Поочередно вставлем элементы

intnActual = _s.m_pTop - _s.m_pData;

for( inti = 0; i < nActual; i++ )

push( _s.m_pData[ i ] );

}

 

//*****************************************************************************

 

// Реализация конструктора перемещения

template< typenameT >

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< typenameT >

Stack< T >::~Stack ()

{

delete[] m_pData;

}

 

//*****************************************************************************

 

// Реализация оператора копирующего присвоения

template< typenameT >

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

{

// Защита от присвоения на самого себя

if( this== & _s )

return* this;

 

// Освобождаем старый блок и выделяем новый

delete[] m_pData;

m_size = _s.m_size;

m_pData = newT[ m_size ];

 

// Копируем полезные данные из другого стека

intnActual = _s.m_pTop - _s.m_pData;

for( inti = 0; i < nActual; i++ )

m_pData[ i ] = _s.m_pData[ i ];

 

// Выставляем вершину стека в аналогичную другому стеку позицию

m_pTop = m_pData + nActual;

 

// Возвращаем ссылку на себя

return* this;

}

 

//*****************************************************************************

 

// Реализация оператора перемещающего присвоения

template< typenameT >

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< typenameT>

voidStack< T >::push ( constT& _value )

{

// Стек не должен быть заполнен на 100% в данный момент

if( isFull() )

throwstd::logic_error( "Stack overflow error" );

 

// Размещаем новое значение в стеке и увеличиваем указатель-вершину

* m_pTop++ = _value;

}

 

//*****************************************************************************

 

// Реализация метода удаления значения с вершины стека

template< typenameT >

voidStack< T >::pop ()

{

// Стек не должен быть пустым в данный момент

if( isEmpty() )

throwstd::logic_error( "Stack underflow error" );

 

// Уменьшаем указатель-вершину

m_pTop--;

}

 

//*****************************************************************************

 

// Реализация метода доступа к значению на вершине стека

template< typenameT >

T& Stack< T >::top () const

{

// Стек не должен быть пустым в данный момент

if( isEmpty() )

throwstd::logic_error( "Stack is empty" );

 

// Возвращаем ссылку на значение, находящееся под указателем-вершиной

return*( m_pTop - 1 );

}

 

//*****************************************************************************

 

// Реализация метода определения пустоты стека

template< typenameT >