Шаблоны компонентных функций 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Шаблоны компонентных функций



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

Шаблон компонентных функций может быть определен как внутри класса (шаблона классов), так и за его пределами. Формат «внутреннего» определения при этом ничем не отличается от формата определения обычного шаблона функций и имеет вид:

template <template_parameter_list>

value_type function_template_id(function_parameter_list) { statements }

где function_template_id – имя шаблона функций; function_parameter_list – список параметров функции; value_type – тип возвращаемого функцией значения; statements – операторы тела шаблона. При внешнем определении необходима полная квалификация имени шаблона функций и указание списков параметров как для шаблона функций, так и для шаблона классов (если шаблон функций входит в обычный класс, то, очевидно, нужны только параметры шаблона функций). Формат внешнего определения шаблона функций при его вхождении в шаблон классов следующий:

template <class_template_parameter_list>
template <function_template_parameter_list>
value_type class_template_id<class_template_arguments>

::function_template_id(function_parameter_list) { statements }

где class_template_parameter_list и function_template_parameter_list – списки параметров, соответственно, шаблона классов и шаблона компонентных функций; class_template_id<class_template_arguments> – полный идентификатор шаблона классов; function_template_id – имя шаблона компонентных функций (остальные элементы формата очевидны).

Ниже приведен пример определения шаблона классов с шаблонами компонентных функций.

Пример

template <class T> class CMyString { // строка символов

public:

// шаблон с внешним определением (сравнение строк)

template <class T2> int compare(const CMyString<T2> &);

// шаблон с внутренним определением (конструктор копирования)

template <class T2> CMyString(const CMyString<T2> & s2) { /*... */ }

...

};

// внешнее определение шаблона компонентных функций

template <class T> template <class T2>

int CMyString<T>::compare(const CMyString<T2> & s2)

{ /*... */ }

Используя шаблоны компонентных функций, нужно помнить об ограничениях, накладываемых на них стандартом языка С++. Так, шаблоны компонентных функций не могут быть виртуальными; их нельзя вводить в локальные классы. Кроме того, их нельзя использовать в качестве деструкторов (но можно в качестве конструкторов). Если шаблон компонентной функции при каком-то сочетании аргументов генерирует функцию, совпадающую по сигнатуре с обычной компонентной функцией, то последней при вызове отдается «предпочтение». Шаблоны компонентных функций могут входить во вложенные классы и шаблоны классов, а также являться дружественными по отношению к другим классам и шаблонам классов.


Специализация шаблонов

Специализация шаблона классов – это механизм, в какой-то мере противоположный самому механизму шаблонов. Действительно, цель введения шаблона – построить обобщенный вариант класса, функции или компонента. Цель специализации – определение частного случая для заданного шаблона. Специализация необходима тогда, когда определение класса (функции, компонента), инстанцируемого от шаблона, отличается от общего случая, задаваемого этим шаблоном. Например, свойства класса векторов CVector<bool>, вполне вероятно, будут отличны от свойств классов CVector<double> и CVector<int>. Например, CVector<bool> может инкапсулировать поразрядные логические операции И, ИЛИ, НЕ и исключающее ИЛИ. Подобные операции вряд ли имеют смысл для классов CVector<double> и CVector<int>. Исходя из этого, после определения шаблона CVector<T> целесообразно дать специализацию CVector<bool>, в которой, в свою очередь, ввести иное определение поведения векторов.

Для специализации[23] шаблонного класса используется заголовок вида template< >, за которым следует определение класса для заданных аргументов шаблона. Специализация возможна лишь после того, как определен или, по крайней мере, объявлен сам шаблон, но до того, как осуществляется инстанцирование шаблона для аргументов специализации. Формат специализации шаблонного класса таков:

template<> class_key template-id class_declaration

где template-id представляет собой полное имя шаблонного класса для аргументов специализации: class_template_id <class_template_argument_list>.

Пример

template<class T> class CStream; // описание шаблона потоков

...

template<> class CStream<char> { /*... */ }; // специализация для char

После такой специализации класс CStream<char> будет использован для представления потоков символов char; для потоков других типов будут использоваться классы, непосредственно (явно либо неявно) инстанцируемые от шаблона.

Приведенный ниже пример показывает более детально, как определить специализацию шаблона CVector, представляющего векторы, для компонент типа bool.

template <class T> class СVector // определение шаблона

{

T * __data; // данные вектора

int __size; // длина вектора

public:

Explicit СVector(int); // конструктор

CVector(const CVector<T> &); // копирующий конструктор

~CVector() { delete[] __data; } // деструктор

T& Item (int i) { return __data[i]; } // получение i–го элемента

...

};

template <> class СVector<bool> // специализация

{

bool * __data; // данные

int __size; // размер

public:

Explicit CVector(int); // конструктор

CVector(const CVector<bool> &); // копирующий конструктор

~CVector() { delete[] __data; } // деструктор

bool Item (int i) { return __data[i]; } // получение элемента

CVector<bool> NOT(void) const; // поразрядное отрицание

CVector<bool> AND(const CVector<bool> &) const; // поразрядное И

...

};

// внешнее определение функции

CVector<bool> CVector<bool>::NOT(void) const

{

CVector<bool> Result(*this);

for(int i = 0; i < __size; i++)

Result.__data[i] =!Result.__data[i];

return Result;

}

...

Специализация шаблона может быть полной или частичной. Приведенный выше пример – полная специализация. При частичной специализации, в отличие от полной, сохраняется «элемент» обобщения; иными словами, частичная специализация – это то же шаблон. Отличие частичной специализации от полной по формату касается только имени: в частичной специализации именем является полный идентификатор шаблона, а в полной – обычный идентификатор. Ниже даны примеры частичных специализаций шаблона классов:

// исходный шаблон

template <class T1, class T2, int I> class A { /*... */ };

// частичная специализация 1

template <class T1, class T2, int I> class A<T1*,T2,I> { /*... */ };

// частичная специализация 2

template <class T, int I> class A<T*,T,I> { /*... */ };

// частичная специализация 3

template <class T> class A<int,T*,5> { /*... */ };

Специализация (как полная, так и частичная) допустима не только для шаблонных классов в целом, но и для их отдельных компонент: например, статических данных, функций, шаблонов компонентных функций и вложенных шаблонов классов. Формат полной специализации для компонентной функции следующий:

template<>

value_type class_template_id<class_template_argument_list>

::function_id(function_parameter_list) { statements }

Пример

template <class T> class OneClass { // определение шаблона

public:

void f(const T &);

...

};

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

template <> void OneClass<AnotherClass>::f(const AnotherClass & o) { }

После такой специализации функция f класса OneClass будет вести себя иным способом, если аргументом шаблона будет класс AnotherClass. При других типах аргумента поведение функции будет определяться ее обобщенным внешним определением.

Формат полной специализации шаблона компонентных функций имеет следующий вид:

 

template<> template<>

value_type class_template_id<class_template_argument_list>

::function_template_id<function_template_argument_list>

(function_parameter_list) { statements }

 

Ниже представлены еще несколько примеров полной и частичной специализации.

Пример

template<class T> struct A { // исходный шаблон

void f(T &);

void h(const T *);

template<class X> void g(const T &, X *);

};

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

template<> void A<int>::f(int & ri) { /*... */ }

// определение компонентного шаблона

template<class T> template<class X>

void A<T>::g(const T & crt, X * px) { /*... */ }

// частичная специализация компонентного шаблона

template<> template<class X>

void A<double>::g(const double & crd, X * px) { /*... */ }

// полная специализация компонентного шаблона

template<> template<>

void A<double>::g<int>(const double & crd, int * pi) { /*... */ }

template<> template<>

void A<double>::g(const double & crd, char * pi) { /*... */ }

// неявно выводится A<double>::g<char>

 


Вопросы для самопроверки

 

1. Какие понятия предметной области можно представить шаблонным классом? Сформулируйте общее определение и приведите несколько примеров.

2. Запишите общий формат определения шаблонного класса.

3. Какую роль играют параметры шаблонного класса?

4. Какие компоненты могут входить в шаблонный класс?

5. Приведите общий формат внешнего определения компонентной функции шаблонного класса.

6. В шаблонном классе template <class elem_type> class Matrix есть компонентная функция Matrix Sum(const Matrix<elem_type> & m2) const; Постройте заголовок внешнего определения этой функции.

7. Как правильно определить и инициализировать статический компонент шаблонного класса? Приведите пример.

8. Почему определение компонент шаблонного класса следует размещать в том же файле, где определен шаблон? Обоснуйте ответ.

9. Что такое инстанцирование шаблонного класса?

10. Чем отличается явное инстанцирование от неявного?

11. Назовите виды параметров и аргументов шаблонных классов и охарактеризуйте назначение каждого из них.

12. Перечислите основные ограничения, накладываемые на ординарные параметры и аргументы шаблонов.

13. Приведите пример предварительного объявления шаблонного класса с шаблонным параметром.

14. Как задаются значения по умолчанию для параметров шаблона разных видов? Приведите примеры.

15. Заголовок шаблонного класса, представляющего поименованные матрицы, записан следующим образом: template <template <class char_type> class matrix_name, class elem_type = int, size_t default_nrows = 3, size_t default_ncols = 3> class EnhancedMatrix; Возможно ли инстанцирование этого шаблона в виде EnhancedMatrix<>? Обоснуйте ответ.

16. Каково назначение шаблонов компонентных функций?

17. Запишите общий формат декларации шаблонов компонентных функций.

18. Постройте формат внешнего определения шаблонов компонентных функций, принадлежащего обычному классу и шаблонному классу.

19. Имеется шаблонный класс template <class elem_type> class SpecialMatrix { /*... */ }, содержащий шаблон копирующего конструктора: template <class elem_type2> SpecialMatrix(const SpecialMatrix<elem_type2> & sample); Как следует записать заголовок этого конструктора во внешнем определении?

20. Почему в теле шаблона копирующего конструктора невозможно обращение к закрытой и защищенной секциям копируемого объекта? Как устранить эту проблему?

21. Что понимается под специализацией шаблона?

22. В чем состоят отличия между полной и частичной специализацией?

23. Запишите общий формат полной специализации шаблонного класса.

24. Покажите на примере, как выполняется полная специализация статического компонента шаблонного класса.

25. Дан заголовок внешнего определения шаблона компонентной функции: template <class elem_type> template <class elem_type2> SpecialMatrix <elem_type> SpecialMatrix <elem_type>:: Mult(const SpecialMatrix <elem_type2> & m2) const (этот шаблон перемножает матрицы с элементами типов elem_type и elem_type2). Выполните частичную специализацию этого шаблона для случая, когда матрица *this имеет вещественные элементы (операторы тела специализации можно не приводить).

26. Каким образом можно построить производный класс от полной специализации шаблонного класса? Приведите набросок определения.

27. Запишите общий формат частичной специализации шаблона компонентных функций и приведите пример.

 

Задачи

 

1. Написать шаблонный класс, описывающий конечные множества с произвольным универсумом. Создать проект, в котором этот класс используется.

2. Разработать шаблонный класс для представления прямоугольных матриц с произвольным типом элемента. Выполнить полную специализацию этого шаблона для элементов типа char (алгоритмы компонентных функций шаблона и специализации предложить самостоятельно). Показать использование шаблонного класса и специализаций в проекте.

3. Написать шаблонный класс для описания бинарных деревьев с произвольным типом вершин. Выбрать какие-нибудь компоненты класса и выполнить их полную и/или частичную специализацию. Показать использование шаблонного класса и специализаций в проекте.

 


Перегрузка операций

Объектная модель С++ дает широкие возможности программисту в приспособлении средств языка к новым потребностям. Одной из таких возможностей является перегрузка (overloading) стандартных операций. Если при разработке приложения оказывается, что некоторая операция не применима к операндам пользовательского типа (класса) или ее поведение отличается от требуемого, то ее действие можно расширить (скорректировать) на такие операнды. Расширение действия сводится к определению для операции нового алгоритма, который будет использоваться при наличии операндов пользовательского класса. Это и составляет сущность перегрузки операций в С++.

6.1. Назначение перегрузки операций и ее реализация в С++

Необходимость перегрузки операций возникает весьма часто. Пусть, например, в приложении определен класс матриц (см. задачи предыдущих глав) и необходимо сделать так, чтобы отдельные матрицы можно было перемножать, используя традиционный синтаксис A*B, характерный для встроенных типов. Для этого достаточно лишь расширить действие бинарной операции * на класс матриц путем ее перегрузки. В результате выражения типа A*B, где оба операнда – матрицы станут обрабатываться по алгоритму, предусмотренному перегрузкой[24].

Для перегрузки некоторой стандартной операции, обозначаемой символом @[25], необходимо определить соответствующую функцию-операцию (operator function). Эта функция-операция будет содержать запись нового алгоритма операции @; он будет применяться при обработке операндов (или операнда) пользовательского класса. Для того, чтобы обеспечить связь функции-операции с указанным классом, необходимо либо использовать этот класс в декларации ее параметров, либо сделать ее компонентом этого класса.

Форматы прототипа и определения функции-операции для перегрузки операции @ в общем виде выглядят следующим образом:

value_type operator @ (parameter_list);

value_type operator @ (parameter_list) { statements }

где operator – ключевое слово, предназначенное для обозначения функций-операций; value_type, parameter_list, statements – соответственно тип значения, список параметров и операторы тела функции-операции. В списке параметров функции-операции, в зависимости от характера, «арности» (местности) операции и способа определения функции, может быть от нуля до произвольного конечного числа параметров[26] (в большинстве случаев, однако, число параметров не превышает двух).

Функцию-операцию всегда можно интерпретировать и вызвать как обычную функцию (в «функциональной» форме). Форматы «функционального» вызова функции-операции таковы:

// operator @ – компонентная функция

object_id.operator @(argument_list);

pointer_to_object_id –> operator @(argument_list)

(*pointer_to_object_id).operator @(argument_list)

// operator @ – глобальная функция

operator @(argument_list);

где object_id, pointer_to_object_id – имена соответственно объекта и указателя на объект, для которого вызывается функция-операция. Однако гораздо более удобна неявная, «операционная» форма вызова, при которой применяется привычный синтаксис работы с операциями:

@ argument // для унарной префиксной операции

argument @ // для унарной постфиксной операции

argument1 @ argument2 // для бинарной операции

Приведенный ниже пример показывает прототипы нескольких компонентных функций-операций, введенных для класса целочисленных прямоугольных матриц[27]. Далее дается иллюстрация к использованию этих функций.

Пример

class CIntMatrix { // класс целочисленных матриц

public:

...

// сумма матриц

const CIntMatrix operator + (const CIntMatrix & m2) const;

// произведение матриц

const CIntMatrix operator * (const CIntMatrix & m2) const;

// присваивание

CIntMatrix & operator = (const CIntMatrix & m2);

// умножение элементов на -1

const CIntMatrix & operator - ();

...

};

...

// использование функций-операций в операционной форме

{

CIntMatrix m1(/*...*/),m2(/*...*/),m3(/*...*/);

...

m3 = m1 + (-m2) * m3;

...

}

// использование функций-операций в функциональной форме

{

CIntMatrix m1(/*...*/),m2(/*...*/),m3(/*...*/);

...

// эквивалентная функциональная форма оператора m3 = m1 + (-m2) * m3;

m3.operator=(m1.operator+((m2.operator-()).operator*(m3)));

...

}

...

Прежде чем более детально обсуждать способы перегрузки операций, остановимся на ряде наиболее важных ограничений механизма перегрузки операций С++.

Во-первых, перегружать можно только стандартные операции; вводить новые символы операций С++ не позволяет. Во-вторых, перегрузке в С++ подлежат не все стандартные операции. К числу операций, не подлежащих перегрузке, относятся следующие:

. (уточнение имени);

.* (прямой доступ через указатель на компонент);

:: (уточнение контекста);

?: (условная операция);

# (запрет обработки аргументов макросов);

## (конкатенация лексем);

sizeof (вычисление размера операнда);

typeid (идентификация типа);

new, new[], delete, delete[] (операции управления памятью);

static_cast, dynamic_cast, const_cast, reinterpret_cast (операции преобразования типов). Остальные операции могут быть перегружены, причем такие операции, как &, *, +, –, допускают перегрузку как унарной, так и бинарной форм. В-третьих, то, что операция подлежит перегрузке, не означает, что она может быть всегда перегружена грамотно и безопасно; имеется ряд операций, безопасная перегрузка которых труднореализуема (их перечень будет рассмотрен далее). В-четвертых, при перегрузке нельзя изменить местность операции; нельзя также сменить уровень ее приоритета, ассоциативность и смысл содержащих ее выражений.

Теперь перейдем к детальному изучению способов перегрузки операций и затронем также некоторые «подводные камни», появляющиеся при перегрузке.

Практически любую из функций-операций можно ввести тремя разными способами: 1) как нестатическую компонентную функцию класса, на который выполняется перегрузка; 2) как глобальную (свободную) функцию, хотя бы один параметр которой имеет тип, связанный с требуемым классом; 3) как глобальную дружественную функцию к указанному классу. Поскольку 2-ой способ в большинстве случаев сводится к 3-му способу, фактически имеется два способа перегрузки: либо глобальной, либо нестатической компонентной функцией-операцией. Ниже отдельно рассматриваются варианты и правила перегрузки унарных, а затем бинарных операций (те операции, при перегрузке которых есть специальные ограничения, выделяются отдельно).


Перегрузка унарных операций

Любую префиксную унарную операцию @ можно перегрузить, определив либо нестатическую компонентную функцию-операцию без параметров, либо глобальную (возможно, дружественную) функцию с одним параметром. Форматы заголовков функций-операций в названных случаях будут следующими:

value_type operator @ (); // компонентная функция

value_type operator @ (parameter); // глобальная функция

friend value_type operator @ (parameter); // дружественная функция

где parameter – декларация параметра функции-операции (параметр должен быть связан с классом C, на который выполняется перегрузка операции, например, const C &); value_type – в общем случае произвольный тип возвращаемого значения (часто он также связан с классом C).

Если функция-операция определена как компонентная функция, то запись @x (x – объект класса C) будет интерпретирована как x.operator@() (в роли операнда операции выступает *this). Если же используется глобальная функция-операция, то имеет место интерпретация operator@(x) (операнд поступает в качестве единственного аргумента).

Ниже, для примера, дано определение функции-операции, перегружающей унарную операцию! для стандартного шаблонного класса basic_ios (эта операция используется для проверки корректности работы с потоком ввода-вывода):

template<class charT, class traits>

inline bool basic_ios<charT, traits>::operator!() const

{

return fail();

}

Особый случай при перегрузке унарных операций составляют операции инкремента и декремента (++ и ––). Особенность их перегрузки состоит в том, что они кроме префиксной имеют еще и постфиксную форму, и необходимо обеспечить четкое различие этих форм при вызове. Для того чтобы этого добиться, стандарт предусматривает определение постфиксных форм с дополнительным фиктивным параметром типа int. При определении функции operator++ (или operator––) внутри класса она не должна иметь параметров, если ее форма префиксная, и должна иметь параметр типа int в постфиксной форме. Если же operator++ (или operator––) определяется за пределами класса, то она должна иметь один параметр в префиксной форме и два параметра, второй из которых типа int, – в постфиксной форме. Чтобы пояснить все сказанное, ниже дается пример определения функции operator++ в разных вариантах[28]:

class X {

public:

...

// компонентные функции-операции

X & operator ++(); // префиксная форма

const X operator ++(int); // постфиксная форма

...

};

class Y {

...

};

// глобальные функции-операции

Y & operator ++(Y &); // префиксная форма

const Y operator ++(Y &, int); // постфиксная форма

При вызове функции operator++ (operator––) используется один из четырех возможных вариантов:

void SomeFunction(X & a, Y & b) {

++a; // вызывается a.operator ++()

a++; // вызывается a.operator ++(0)

++b; // вызывается operator ++(b)

b++; // вызывается operator ++(b,0)

}

Как видно из комментариев примера, в случае постфиксной формы вместо фиктивного параметра подставлен нуль, что как раз и позволяет отличить использование префиксной формы от постфиксной[29].

Кроме проблемы различия префиксной и постфиксной форм при вызове, перегрузка операций ++ и –– имеет и другие особенности. Например, как правильно задать возвращаемые значение? Как показывает анализ, наиболее подходящий вариант такой: для префиксной формы – ссылка на объект, а для постфиксной – константный объект. Действительно, префиксная операция ++ (и ––) должна изменить объект-операнд (*this или первый аргумент) и возвратить его же в измененном виде. Постфиксная операция ++ (––) должна изменить операнд, но вернуть его старое значение. Константность возвращаемому объекту необходимо придать для того, чтобы не дать программисту делать «глупости» вида X++ = Y или X++++.

Из соображений эффективности и гибкости обычно постфиксную форму operator++ (––) реализуют на основе префиксной формы с тем, чтобы в будущем поддерживать только префиксную[30]. Ниже дано определение функции operator++(int), реализованной через operator++():

const X X::operator ++(int)

{

X old(*this); // запомнить старый объект

++*this; // изменить объект, вызывая operator++()



Поделиться:


Последнее изменение этой страницы: 2016-08-14; просмотров: 311; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 18.226.93.207 (0.631 с.)