Лекции.Орг


Поиск:




Категории:

Астрономия
Биология
География
Другие языки
Интернет
Информатика
История
Культура
Литература
Логика
Математика
Медицина
Механика
Охрана труда
Педагогика
Политика
Право
Психология
Религия
Риторика
Социология
Спорт
Строительство
Технология
Транспорт
Физика
Философия
Финансы
Химия
Экология
Экономика
Электроника

 

 

 

 


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




 

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

 

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 >





Поделиться с друзьями:


Дата добавления: 2017-01-21; Мы поможем в написании ваших работ!; просмотров: 639 | Нарушение авторских прав


Поиск на сайте:

Лучшие изречения:

Если вы думаете, что на что-то способны, вы правы; если думаете, что у вас ничего не получится - вы тоже правы. © Генри Форд
==> читать все изречения...

2319 - | 2226 -


© 2015-2025 lektsii.org - Контакты - Последнее добавление

Ген: 0.013 с.