
16. Полная специализация шаблонов.
По умолчанию шаблон представляет единственное определение, которое должно использоваться для всех аргументов шаблона (или комбинаций аргументов шаблона). Однако иногда возникает желание использовать одну реализацию шаблона, когда в качестве аргумента шаблона фигурирует, например, указатель, другую – когда аргумент указателем не является. Это можно реализовать, обеспечив альтернативные определения шаблона и предоставив компилятору возможность осуществлять выбор нужного варианта на основе аргументов шаблона, указанных при его использовании. Такие альтернативные определения шаблона называются специализациями, определяемыми пользователем, или просто пользовательскими специализациями.
Специализация — это переопределение шаблона для конкретного значения параметра.
Специализация позволяет изменить поведение шаблона для конкретных типов данных (полная специализация) или для определенных групп типов данных (частичная специализация).
Исходный шаблон, который специализируется, называется базовым.
Пример (из лекций):
//первичный шаблон
template <class T>
class vec <T*>
{
void set (unsigned i, T* item);
}
//специализированный шаблон
template <>
class vec <int*>
{
void set (unsigned i, int* item);
}
Внешнее определение функции для явной специализации шаблона
void vec <int*>::set (unsigned i, int* item)
{ delete _data[i];
_data [i]=item;
}
Состав функции специализации и базового шаблона могут различаться.
17. Частичная специализация шаблонов.
Частичная специализация позволяет изменить поведение базового шаблона для группы типов. Например для указателя на производный тип или для ссылки на производный тип.
template <class T>
class vec <T*>
{
Void set (unsigned I, t* item);
}
template <class T>
void vec <T*>::set (…)
{delete _data [i]
_data [i]=item;
}
Специализация членов шаблона
Она позволяет применять поведение отдельных функций шаблона без необходимости модификации шаблонов в целом.
Пример: специализированная функция (частная специализация).
Интерфейс базового шаблона остается неизменным, а специализация делается путем внешнего определения необходимых функций.
18. Перегрузка операций. Основные понятия.
В языке С++ реализован механизм перегрузки операций. Такая перегрузка представляет собой еще один пример полиморфизма. Перегрузка операции предоставляет возможность рассматривать операции языка C++ сразу в нескольких смыслах. Фактически часть операций языка C++ уже перегружены изначально. Например, операция *, будучи примененной к адресу, дает значение, которое хранится по этому адресу. Однако, применяя оператор * к паре числовых значений, получаем произведение этих значений. Таким образом, в данном случае в языке C++ используется количество и типы операндов, чтобы решить, какое действие предпринять.
Больше дополнительная информация:
Для операторов языка С++ предусматриваются два дополнительных свойства, существенным образом влияющих на их функциональность: приоритет и ассоциативность. Действительно, встает вопрос как при записи понимать: a+b*c – как (a+b)*c или как a+(b*c)? Выражение a-b+c — это (a-b)+c или a-(b+c)?).
Приоритет (ранг или старшинство оператора) — формальное свойство оператора, влияющее на очередность его выполнения в выражении с несколькими различными операторами при отсутствии явного (с помощью скобок) указания на порядок их вычисления. Например, оператор умножения * обладает изначально более высоким приоритетом, чем оператор сложения +, а потому в выражении a+b*c будет получено сначала произведение b и c, а потом уже сумма.
Операторы могут иметь одинаковый приоритет, тогда они вычисляются по правилу ассоциативности, установленному для них. В программировании ассоциативностью операторов называют последовательность (очередность) их выполнения в случае, когда операторы имеют одинаковый приоритет и отсутствует явное (с помощью скобок) указание на очерёдность их выполнения. Причем различается левая ассоциативность, при которой вычисление выражения происходит слева–направо, и правая ассоциативность — справа–налево. Соответствующие операторы называют левоассоциативными и правоассоциативными.
В языках программирования (в том числе в языке С++) применяются два способа задания приоритетов операторов:
Первый из них связан с распределением всех операторов по иерархии приоритетов. Этот способ всегда используется для задания приоритетов по умолчанию и фиксируется в описании языка в виде соглашения, что соответствующим операторам присваивается определенные приоритеты.
Второй способ дает возможность изменять приоритеты по умолчанию, определяя их в явном виде с помощью парных круглых скобок. При этом глубина вложенности прямо пропорциональна величине приоритета, то есть более вложенные внутренние скобки указывают на более высокий приоритет, чем внешние, обрамляющие их.
Поскольку для встроенных в язык операторов всегда предусматриваются приоритеты и ассоциативность, то возникает вопрос: какие приоритеты и ассоциативность будут иметь переопределённые версии этих операторов или, тем более, новые созданные программистом операторы?
Кроме того, имеются и другие “тонкие моменты”, которые могут требовать уточнения. Например, в языке С++ существуют две формы операторов инкремента (увеличения) и декремента (уменьшения) значения ++ и -- — префиксная и постфиксная, поведение которых различается. Как должны вести себя перегруженные версии таких операций? Различные языки по-разному решают приведённые вопросы.
В языке C++ приоритет и ассоциативность перегруженных версий операций сохраняются такими же, как и у определённых в языке, а описания перегрузки префиксной и постфиксной формы операторов инкремента и декремента используют различные сигнатуры.
Итак!!! Перегрузка операций — это возможность назначать новый смысл операциям при использовании их с определенным классом.
Использование механизма перегрузки операций позволяет повысить удобочитаемость программ и облегчить их понимание, выражая операции класса в более понятном виде. При этом:
Чтобы перегрузить операцию, необходимо определить класс, которому операция будет назначена;
Когда перегружаете операция, то перегрузка действует только для класса, в котором она определяется. Если программа использует операцию с неклассовыми переменными (например, переменными типа int или float), используется стандартное определение операции;
Чтобы перегрузить операцию класса, необходимо использовать ключевое слово языка C++ operator для определения метода класса, который вызывается каждый раз, когда переменная класса использует операцию;
В языке С++ разрешается перегружать следующие операций:
+ - * / % ^ & | ~ !
= < > += -= *= /= %= ^= &=
|= << >> >>= <<= == != <= >= &&
|| ++ -- ->* , -> [] () new new [] delete delete []
C++ не позволяет вашим программам перегружать оператор выбора элемента (.), оператор указателя на элемент (.*), оператор разрешения области видимости (::), условный оператор сравнения (?:), оператор sizeof(), tipeid().
Рассмотрим пример.
Пусть заданы множества А и В:
А = { а1, а2, а3 }; В = { a3, a4, a5 },
и мы хотим выполнить операции объединения (+) и пересечения (*) множеств.
А + В = { a1, a2, a3, a4, a5 } А * В = { a3 }.
Можно определить класс Set - "множество" и определить операции над объектами этого класса, выразив их с помощью знаков операций, которые уже есть в языке С++, например, + и *. В результате операции + и * можно будет использовать как и раньше, а также снабдить их дополнительными функциями (объединения и пересечения). Как определить, какую функцию должен выполнять оператор: старую или новую? Очень просто – по типу операндов. А как быть с приоритетом операций? Сохраняется определенный ранее приоритет операций. Для распространения действия операции на новые типы данных надо определить специальную функцию, называемую "операция-функция" (operator-function). Ее формат:
тип_возвр_значения operator знак_операции(специф_параметров)
{операторы_тела_функции}
При необходимости может добавляться и прототип:
тип_возвр_значения operator знак_операции(специф_параметров);
Если принять, что конструкция operator знак_операции есть имя некоторой функции, то прототип и определение операции-функции подобны прототипу и определению обычной функции языка С++. Определенная таким образом операция называется перегруженной (overload).
Чтобы была обеспечена явная связь с классом, операция-функция должна быть либо компонентом класса, либо она должна быть определена в классе как дружественная и у нее должен быть хотя бы один параметр типа класс (или ссылка на класс). Вызов операции-функции осуществляется так же, как и любой другой функции С++: operator @. Однако разрешается использовать сокращенную форму ее вызова: a @ b, где @ - знак операции.
Основные правила перегрузки операций.
Вводить собственные обозначения для операций, не совпадающие со стандартными операциями языка С++, нельзя.
Не все операции языка С++ могут быть перегружены. Нельзя перегрузить следующие операции: . – прямой выбор компонента, .* – обращение к компоненту через указатель на него, ? : – условная операция, :: – операция указания области видимости, sizeof, # и ## – препроцессорные операции.
Каждая операция, заданная в языке, имеет определенное число операндов, свой приоритет и ассоциативность. Все эти правила, установленные для операций в языке, сохраняются и для ее перегрузки, т.е. изменить их нельзя.
Любая унарная операция @ определяется двумя способами: либо как компонентная функция без параметров, либо как глобальная (возможно дружественная) функция с одним параметром. Выражение @z означает в первом случае вызов z.operator @(), во втором - вызов operator @(z).
Любая бинарная операция @ определяется также двумя способами: либо как компонентная функция с одним параметром, либо как глобальная (возможно дружественная) функция с двумя параметрами. В первом случае x @ y означает вызов x.operator @(y), во втором – вызов operator @(x, y).
Перегруженная операция не может иметь аргументы (операнды), заданные по умолчанию.
В языке С++ установлена идентичность некоторых операций, например, ++z – это тоже, что и z += 1. Эта идентичность теряется для перегруженных операций.
Функцию operator можно вызвать по ее имени, например, z = operator * x, y) или z = x.operator *(y). В первом случае вызывается глобальная функция, во втором – компонентная функция класса X, и x – это объект класса X. Однако, чаще всего функция operator вызывается косвенно, например, z = x * y.
За исключением перегрузки операций new и delete функция operator должна быть либо нестатической компонентной функцией, либо иметь как минимум один аргумент (операнд) типа "класс" или "ссылка на класс" (если это глобальная функция).
Операции '=', '[]', '–>' можно перегружать только с помощью нестатической компонентной функции operator @. Это гарантирует, что первыми операндами будут леводопустимые выражения.
Операция '[]' рассматривается как бинарная. Пусть a – объект класса A, в котором перегружена операция '[]'. Тогда выражение a[i] интерпретируется как a.operator [](i).
Операция '()' вызова функции рассматривается как бинарная. Пусть a – объект класса A, в котором перегружена операция '()'. Тогда выражение a(x1, x2, x3, x4) интерпретируется как a.operator ()(x1, x2, x3, x4).
Операция '–>' доступа к компоненту класса через указатель на объект этого класса рассматривается как унарная. Пусть a – объект класса A, в котором перегружена операция '–>'. Тогда выражение a–>m интерпретируется как (a.operator–>())–>m. Это означает, что функция operator –>() должна возвращать указатель на класс A, или объект класса A, или ссылку на класс A.
Перегрузка операций '++' и '--', записываемых после операнда (z++, z--), отличается добавлением в функцию operator фиктивного параметра int, который используется только как признак отличия операций z++ и z-- от операций ++z и --z.
Глобальные операции new можно перегрузить и в общем случае они могут не иметь аргументов (операндов) типа "класс". В результате разрешается иметь несколько глобальных операций new, которые различаются путем изменения числа и (или) типов аргументов.
Глобальные операции delete не могут быть перегружены. Их можно перегрузить только по отношению к классу.
Заданные в самом языке глобальные операции new и delete можно изменить, т.е. заменить версию, заданную в языке по умолчанию, на свою версию.
Локальные функции operator new() и operator delete() являются статическими компонентами класса, в котором они определены, независимо от того, использовался или нет спецификатор static (это, в частности, означает, что они не могут быть виртуальными).
Для правильного освобождения динамической памяти под базовый и производный объекты следует использовать виртуальный деструктор.
Если для класса X операция "=" не была перегружена явно и x и y - это объекты класса X, то выражение x = y задает по умолчанию побайтовое копирование данных объекта y в данные объекта x.
Функция operator вида operator type() без возвращаемого значения, определенная в классе A, задает преобразование типа A к типу type.
За исключением операции присваивания '=' все операции, перегруженные в классе X, наследуются в любом производном классе Y.
Пусть X – базовый класс, Y – производный класс. Тогда локально перегруженная операция для класса X может быть далее повторно перегружена в классе Y.