Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

4 семестр / Методические материалы / metodicheskie-ukazaniia-po-teme-29

.pdf
Скачиваний:
0
Добавлен:
20.05.2025
Размер:
636.79 Кб
Скачать

НАЦИОНАЛЬНЫЙ ИССЛЕДОВАТЕЛЬСКИЙ ЯДЕРНЫЙ УНИВЕРСИТЕТ «МИФИ» Кафедра информатики и процессов управления (№17)

Дисциплина «Информатика» (основной уровень), 2-й курс, 4-й семестр.

 

Методические указания

 

 

Тематическое занятие 29

 

 

Шаблоны.

 

 

Содержание

 

1.

Метапрограммирование в языках Си и Си++

....................2

1.1.

Понятие метапрограммирования .......................................................

2

1.2.

Препроцессор языка Си .......................................................................

2

1.3.

Макросы в языке Си .............................................................................

3

1.4.

Шаблоны в языке Си++ как вид полиморфизма..............................

4

2.

Шаблоны функций ...................................................................

5

2.1.

Пример шаблона функции ...................................................................

5

2.2.

Инстанцирование шаблонов функций..................................................

6

2.3.

Специализация шаблонов функций...................................................

8

3.

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

9

3.1.

Пример класса, реализующего стек ...................................................

9

3.2.

Шаблон класса, реализующего стек ................................................

11

3.3.

Инстанцирование шаблонов классов .................................................

12

3.4.

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

15

3.5.

Частичная специализация .................................................................

18

3.6.

Сложные случаи частичной специализации ..................................

18

1

1. Метапрограммирование в языках Си и Си++

1.1. Понятие метапрограммирования

Под метапрограммированием понимается составление такого программного кода, результатом выполнения которого является другой программный код. Например, составляется метакод, который генерирует другой код (возможно, даже на другом языке программирования), выполнение которого позволяет получить решение исходной задачи.

Чаще всего при метапрограммировании генерируется не целая программа, а некоторый фрагмент кода (модуль, функция, класс и др.).

1.2. Препроцессор языка Си

Примером метакода могут считаться макроопределения (макросы), которые в ходе макропроцессирования превращаются во фрагменты текста программы. В языке Си эти действия выполняет специальная программа, которая входит в комплект компилятора, часто называемая препроцессором.

В самом начале компиляции на этапе лексического анализа текст программы на языке Си разбивается на последовательность лексем, в которую не включаются пробельные символы и комментарии. Например, строка кода:

y = 5 + 12*x; // calculation

станет последовательностью лексем:

«y», «=», «5», «+», «12», «*», «x», «;».

В программе можно сделать макроопределение (макрос) в виде константы, задаваемой с помощью директивы компилятора #define :

#define NUM 12

...

y = 5 + NUM*x; // calculation

Тогда последовательность лексем:

«y», «=», «5», «+», «NUM», «*», «x», «;»

после макроподстановки в результате работы препроцессора становится: «y», «=», «5», «+», «12», «*», «x», «;»

Макрос, задаваемый директивой #define, может содержать выражение, например:

#define NUM

10 + 2

// неверно!

...

 

 

y =

5 + NUM*x;

// calculation

В этом случае последовательность лексем после макроподстановки: «y», «=», «5», «+», «10», «+», «2», «*», «x», «;»

станет эквивалентна коду:

y = 5 + 10 + 2*x;

2

и результат вычислений не будет соответствовать ожидаемому.

Поэтому, если макрос содержит не константу, а сложное выражение, то его всегда необходимо заключать во внешние скобки:

#define NUM ( 10 + 2 ) // всегда использовать внешние скобки!

...

y = 5 + NUM*x; // calculation

В этом случае последовательность лексем после макроподстановки:

«y», «=», «5», «+», «(», «10», «+», «2», «)», «*», «x», «;»

эквивалентна корректному коду:

y = 5 + (10 + 2)*x;

1.3. Макросы в языке Си

Рассмотрим более сложный пример, когда в программе необходимо определить максимальное из двух чисел. Для этого можно составить отдельную функцию c тернарной операцией условия и вызвать ее:

int max(int a,

int

b) {

return

a

> b

? a : b;

}

 

 

 

...

 

 

 

int x, y, res;

...

res = max(x, y+5); // Вызов функции.

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

Время выполнения такой программы можно ощутимо уменьшить, если во всей программе заменить вызов функции на содержимое ее тела. Но при такой замене пострадает текст программы – код потеряет все преимущества использования функций.

В подобных случаях в языке Си можно использовать макрос с макропараметрами:

#define MAX(A, B) ( (A) > (B) ? (A) : (B) )

...

res = MAX(x, y+5); // Использование макроса.

В директиве #define после имени макроса MAX ставится открывающаяся круглая скобка (обязательно без пробельных символов), внутри которой указываются список макропараметров (через запятую), затем закрывающаяся круглая скобка.

Чтобы избежать ошибок при подстановке макропараметров следует соблюдать правило: в параметризованном макросе каждое вхождение

макропараметра необходимо заключать в круглые скобки, а все выражение целиком – в еще одни внешние.

3

В рассматриваемом примере при вызове макроса MAX его параметры A и B заменяется соответственно на x и y, и компилируемая последовательность лексем становится подобной следующему коду:

res = ( (x) > (y+5) ? (x) : (y+5) ); // Эквивалентный код

В этом случает время выполнения программы существенно уменьшится по сравнению с использованием функции.

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

В языке Си++ задачу повышения производительности программы, рассмотренную в последнем примере, можно решить за счет использования inline-функций.

Препроцессор языка Си для совместимости включен в комплект компилятора языка Си++. Но в языке Си++ существует другая, более удобная и безопасная, возможность метапрограммирования – шаблоны (template).

1.4. Шаблоны в языке Си++ как вид полиморфизма

Язык Си++ позволяет создавать шаблон (template) – заготовку кода функции (или класса), имеющую параметры. Настоящая функция (или класс) создается, когда происходит подстановка значений параметров шаблона. Шаблон можно использовать для создания нескольких разных функций (или классов), отличающихся между собой значениями параметров, подставляемых в шаблон. Эти значения параметров называют аргументами шаблона.

Шаблон – это еще не код, а заготовка для кода, из которой после подстановки параметров получается настоящий компилируемый код. Поэтому использование шаблонов относится к метапрограммированию.

Вместе с тем, шаблоны представляют собой параметрический полиморфизм как частный случай статического полиморфизма, поскольку реализуются на этапе компиляции программы. Шаблоны не имеют прямого отношения к объектно-ориентированному программированию.

Языки Си и Си++ являются строго типизированными – в них требуется, чтобы все переменные имели определенный тип, явно объявленный в програме или выводимый компилятором. Поэтому в качестве параметров шаблонов чаще всего выступают имена типов или классов.

Различают шаблоны функций и шаблоны классов.

4

2.Шаблоны функций

2.1.Пример шаблона функции

Рассмотрим пример функции, которая сортирует массив чисел типа int методом обмена («пузырька», bubble):

void bsort(int *arr, int len) { for (int i=0; ; ++i) {

bool done = true;

for (int j=len-2; j>=i; --j) { if (arr[j+1] < arr[j]) {

int temp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = temp; done = false;

}

if (done) break;

}

}

Пусть теперь требуется создать такую же функцию для сортировки чисел типа double. Использовать для этого имеющуюся функцию bsort невозможно. Но новая функция сортировки чисел типа double не будет отличаться ничем от имеющийся функции bsort, кроме типов параметра arr и временной переменной temp (в коде выделено синим). В этих двух местах необходимо использовать тип double вместо int.

Обойтись без дублирования кода функции позволяет использование шаблона, параметром которого является тип элементов массива. Параметр шаблона назовем T (от слова type):

template <class T>

void bsort(T *arr, int len) { for (int i=0; ; ++i) {

bool done = true;

for (int j=len-2; j>=i; --j) if (arr[j+1] < arr[j]) {

T temp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = temp; done = false;

}

if (done) break;

}

}

После ключевого слова template в угловых скобках указывается список параметров шаблона, причем class на самом деле означает не только класс, а

любой произвольный тип.

В примере шаблон имеет один параметр с именем T. Такой код возможно откомпилировать только, когда вместо T будет подставлен конкретный тип. Подстановка значений параметров (аргументов) в шаблон делается тоже с помощью угловых скобок:

5

int a[100];

...

bsort<int>(a, 100); // Функция из шаблона с параметром int

...

double m[50];

....

bsort<double>(m, 50); // Функция из шаблона с параметром double

При компиляции создаются две разные функции: bsort<int> и bsort<double>, хотя их объектный код генерируется из одного шаблона.

Если в рассмотренном примере шаблон функции и оба ее вызова с разным значением параметра (bsort<int> и bsort<double>) находятся в одном файле (в одной единице трансляции), то генерация кода обеих функций происходит в момент их вызова. Но описание шаблона, генерирование кода и вызов функции могут быть сделаны в разных файлах реализации (в разных единицах трансляции), тогда работа компилятора с шаблонами происходит несколько сложнее.

2.2. Инстанцирование шаблонов функций

Генерация кода функции (или класса) по шаблону для конкретных значений параметров называется инстанцированием.

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

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

Рассмотрим вариант неявного инстанцирования при вызове функции bsort из предыдущего примера. Пусть код шаблона функции bsort и код обоих ее вызовов с разными значениями параметра T (то есть с аргументами шаблона int и double) содержатся в одном файле (в одной единице трансляции):

main.cpp – основной файл: #include <iostream>

using namespace std;

template <class T>

void bsort(T *arr, int len) {

... // Тело шаблона функции (из предыдущего примера).

}

...

int main() { int a[100];

double m[50];

... // Заполнение значениями массивов a и m. bsort<int>(a, 100); // Неявное инстанцирование bsort<int>.

bsort<double>(m, 50); // Неявное инстанцирование bsort<double>.

...

}

6

Здесь инстанцирование происходит неявно, когда компилятор находит каждый из двух вызовов шаблонной функции bsort.

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

// Автоматическое выведение аргументов шаблона функции: bsort(a, 100); // вызывает функцию bsort<int>; bsort(m, 50); // вызывает функцию bsort<double>.

При этом по типу передаваемых в саму функцию аргументов (a или m), компилятор определяет, какое значение параметра T (аргумент int или double) необходимо использовать. Описанная возможность вызова функции называется

автоматическим выведением аргументов шаблона функции. (Для шаблонов классов подобная возможность присутствует только начиная со стандарта языка C++17.)

Теперь рассмотрим вариант явного инстанцирования функции bsort с помощью ключевого слова template. Создадим модуль для функции bsort, который состоит из заголовочного файла модуля и отдельного файла с реализацией модуля:

объявление шаблона функции bsort выносится в заголовочный файл,

описание реализации шаблона функции помещается в файл реализации,

вызов функции находится в основном файле кода программы.

bsort.h – заголовочный файл модуля:

template

<class T>

 

 

void bsort(T*, int); // Объявление шаблона функции.

 

 

bsort.cpp – файл с

реализацией модуля:

 

 

 

 

#include

"bsort.h"

 

 

template

<class T>

 

 

void bsort(T *arr,

int len) {

 

...

// Тело шаблона функции (из предыдущего примера).

}

 

 

 

template

 

 

 

void

bsort(int*, int);

// Явное инстанцирование для int.

template

 

 

 

void

bsort(double*, int);// Явное инстанцирование для double.

 

 

 

 

main.cpp – основной файл: #include <iostream> #include "bsort.h"

using namespace std;

int main() { int a[100];

double m[50]; long d[75];

... // Заполнение значениями массивов a, m и d. bsort(a, 100); // Вызов инстанцированной функции для int. bsort(m, 50); // Вызов инстанцированной функции для double. bsort(d, 75); // Ошибка: для аргумента "long" функция не

// инстанцирована и не может быть вызвана.

}

7

Разные части кода размещаются в двух различных единицах трансляции (в реализации модуля bsort.cpp, в основной программе main.cpp) и компилируются независимо друг от друга: После компиляции отдельные части объектного кода в процессе компановки соединяются в единую программу, готовую к выполнению.

Компилятор не может провести инстанцирование ни одной функции при ее вызове (в файле main.cpp), поскольку описание шаблона вынесено в отдельный модуль, компилируемый независимо. Поэтому нужные шаблонные функции должны быть заранее инстанцированы в модуле (файл bsort.cpp) явным образом с помощью ключевого слова template.

Успешную компиляцию проходят только вызовы уже инстанциированных функций (для значений параметра T соответствующих int и double). Для значения long компиляция вызова функции bsort(d,75) не выполнится.

2.3. Специализация шаблонов функций

В языке Си++ можно задавать частные (специальные) случаи для параметров шаблона. Это позволяет в нужном конкретном случае генерировать код отдельно заданным образом, а во всех остальных случаях – в соответствии с общим шаблоном.

Допустим, рассмотренный выше шаблон функции сортировки массива (bsort) необходимо использовать для массива указателей на строки (тип const char*), чтобы расположить строки в лексикографическом (алфавитном) порядке.

Если вызвать функцию bsort в виде:

char *a[50];

...

bsort<const char*>(const char *a, 50); // Некорректно из-за "<".

То результат сортировки станет некорректным, поскольку в функции bsort для сравнении элементов массива (arr[j+1] < arr[j]) используется операция сравнения «меньше» ("<"), которая здесь станет сравнивать значения указателей, а не содержание символьных строк.

Для решения этой проблемы вначале опишем шаблон новой функции сравнения (lessthan), которая будет действовать аналогично операции a<b для всех типов, для которых определена операция "<":

template <class T>

bool lessthan(T a, T b) { return a < b;

}

Затем в шаблоне функции bsort заменим операцию сравнения «меньше» ("<") на вызов функции lessthan:

template <class T>

void bsort(T *arr, int len) { for (int i=0; ; ++i) {

bool done = true;

8

for (int j=len-2; j>=i; --j)

if ( lessthan<T>(arr[j+1], arr[j]) ) { T temp = arr[j];

arr[j] = arr[j+1]; arr[j+1] = temp; done = false;

}

if (done) break;

}

}

Наконец опишем шаблон функции сравнения (lessthan) для рассматриваемого частного случая сортировки массива указателей на строки

(тип const char*):

template <>

bool lessthan<const char*>(const char *a, const char *b) { return strcmp(a, b) < 0;

}

Этот шаблон составлен для частного случая и не зависит от параметров, поэтому угловые скобки после template остаются пустыми. После имени шаблонной функции указываются все параметры шаблона (в данном случае один параметр): lessthan<const char*>.

Описание шаблона для частного случая, которое не зависит от параметров, называется специализацией шаблона.

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

3.1.Пример класса, реализующего стек

Чтобы проиллюстрировать использование шаблонов классов, рассмотрим упрощенный пример создания класса для реализации стека на основе линейного односвязного списка. Напомним, что элементами такого списка являются структуры с двумя полями: информационным (value) и указательным (next). Стек – это список, который организован по принципу LIFO (“last in, first out”), и работа с ним происходит через указатель на вершину стека (top):

top

 

 

 

 

СТЕК

 

 

 

 

 

 

 

 

pop

 

 

 

 

 

 

 

 

 

 

 

 

 

 

value

 

value

 

value

 

value

 

 

 

 

push

next

 

next

 

next

 

0

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Пусть элементами стека являются целые числа типа int. Тогда опишем класс Stack, содержащий основные методы работы со стеком: поместить элемент (push), извлечь элемент (pop):

9

class Stack

{

 

 

 

 

struct Elem {

// Элемент стека:

 

int

value;

//

-

информационное поле;

Elem *next;

//

-

указательное

поле.

};

Elem *top; // Поле класса - указатель на вершину стека.

public:

// Методы класса:

Stack(): top(0) { } // - конструктор (обнуляет вершину);

~Stack();

// - деструктор (освобождает память);

void push(int x);

// - поместить элемент x в стек;

void pop();

// - извлечь элемент из стека.

};

 

 

void Stack::push(int x) {

// Добавление в стек элемента

Elem *temp = new Elem;

// со значением x типа int,

temp->value = x;

 

// выделяя динамическую память.

temp->next = top;

 

 

top = temp;

 

 

}

 

 

void Stack::pop() {

// Удаление элемента из вершины стека,

if (top) {

// освобождая динамическую память.

Elem *temp = top;

 

top = temp->next;

 

delete temp;

 

 

}

 

 

}

 

 

Stack::~Stack() {

// Деструктор освобождает всю выделенную

while (top) {

// память путем удаления всех элементов

pop();

// стека с помощью метода pop.

}

 

 

}

 

 

Теперь можно создавать стек как объект класса Stack и пользоваться им с помощью методов класса push и pop:

Stack s;

// Содержимое

стека (начиная с вершины):

s.push(12);

//

12;

 

s.push(345);

//

345, 12;

 

s.push(67);

//

67, 345,

12;

s.pop();

//

345, 12.

 

Для объектов класса перегрузим операцию "<<", которая станет добавлять в стек элемент, аналогично методу push:

class Stack {

...

public:

...

Stack& operator<<(int x); // Перегрузка операции "<<".

};

...

10

Соседние файлы в папке Методические материалы