Использование шаблонов в C++ (исходники)

Источник: Realcoding
Олег Ремизов

Многие программисты, имеющие опыт программирования на С++ и одновременно освоившие языки, производные от С++ (такие, как Java или C#), наверняка заметили, что новые языки имеют встроенные библиотеки разнообразных коллекций, которые делают код более компактным. В то же время в С++ таких возможностей не обнаруживается. "Зачем использовать шаблоны, если есть классы полиморфных коллекций?" - спросят многие, ведь практически каждый компилятор C++ имеет свою фирменную библиотеку.

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

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

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

Предположим, у нас есть функция, выбирающая максимальное значение из массива чисел, имеющих тип double:

double maxdouble (double array [], int length) {
double max = array [0];
for (int i=1; iif (maxmax = array [i];
return max;
}

 

Теперь допустим, что нам необходимо сделать то же самое, но с другим типом данных - например, c типом long. Прототип такой функции:

long maxlong (long array [], int len);

Естественно, что тело функции остается таким же, как и для типа double. При этом имена функций разные. Возникает мысль: а нельзя ли назвать обе функции одним и тем же именем (например max), чтобы не задумываться над тем, какое имя следует писать при вызове функции. А уж какую из них ему вызывать, компилятор пусть сам решает, основываясь на том, какого типа переменные переданы в функцию. Такой механизм реализован в С++ и практически во всех современных объектно-ориентированных языках, и называется он перегрузкой функций. Это ощутимо прибавляет работы компилятору, но облегчает жизнь программиста.

Таким образом, прототипы функции max для параметров разных типов могут выглядеть так:

int max (int array [], int len);
long max (long array [], int len);
double max (double array [], int len);

Attention! Если вы попытаетесь создать два прототипа функций с одинаковыми именами и передаваемыми параметрами, но разными возвращаемыми значениями:

int max (int array [], int len);
long max (int array [], int len);

 

то получите сообщение об ошибке. Таким образом компилятор проинформирует вас, что он не может перегрузить функции с разными возвращаемыми типами, но с одинаковыми типами параметров. Объясняется это, прежде всего, тем, что вызов функции может выглядеть так:

long numbers [] = {1,2,3,3,6,7,11,50,40};
int len = sizeof numbers/sizeof number [0];
...
max (numbers, len);

 

Попробуйте решить сами, какую из двух функций необходимо подставить на место ее вызова. Фактически подходят обе, и компилятор не берет на себя ответственность решать, как поступить "более правильно".

Для иллюстрации вышесказанного далее приведен фрагмент кода, в котором перегружены функции для int, long, double:

#include
using namespace std;

int max (int array [], int len);
long max (long array [], int len);
double max (double array [], int len); // functions

int main ()
{
int small [] = {1,24,34,22};
long medium [] = {23,245,123,1,234,2345};
double large [] = {23.0,1.4,2.456,345.5,12.0,21.0};
int lensmall = sizeof small/sizeof small [0];
int lenmedium = sizeof medium/sizeof medium [0];
int lenlarge = sizeof large/sizeof large [0];
cout << endl << max (small, lensmall);
cout << endl << max (medium, lenmedium);
cout << endl << max (large, lenlarge);
cout << endl;
return 0;
}

int max (int x [], int len)// Maximum of ints
{
int max = x [0];
for (int i=1; iif (maxmax = x [i];
return max;
}

long max (long x [], int len) // Maximum of longs
{
long max = x [0];
for (int i=1; iif (maxmax = x [i];
return max;
}

double max (double x [], int len) // Maximum of doubles
{
double max = x [0];
for (int i=1; iif (maxmax = x [i];
return max;
}

При желании можете попробовать откомпилировать этот текст. Aвтор тестировал примеры для этой статьи на двух компиляторах VC++ 7.0, который входит в состав Visual Studio 7.0, и GNU C++ 3.0 под операционной системой Linux.

Если же у вас нет сейчас компьютера под руками, то на рисунке вы можете увидеть, что именно должно получиться.

Теперь присмотритесь внимательно к коду - тела всех трех функций одинаковы, разняться только типы передаваемого массива. А что если мы подставим вместо конкретного типа данных некоторый абстрактный тип, который, в зависимости от ситуации, будет заменен на реальный тип данных - и, таким образом, мы сможем сгенерировать тот же самый код для какого угодно типа данных?

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

template T max (T x [], int len)
{
T max = x [0];
for (int i=1; iif (maxmax = x [i];
return max;
}

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

template

после чего замените T на double. Фактически сейчас вы выполнили работу препроцессора. В случае если бы он встретил вызов функции с параметром типа double, он сделал бы то же самое, что проделали сейчас вы.

Ниже приведен пример кода с использованием шаблона функции:

#include
using namespace std;
template T max (T x [], int len)
{
T max = x [0];
for (int i=1; iif (maxmax = x [i];
return max;
}

int main ()
{
int small [] = {1,24,34,22};
long medium [] = {23,245,123,1,234,2345};
double large [] = {23.0,1.4,2.456,345.5,12.0, 21.0};
int lensmall = sizeof small/sizeof small [0];
int lenmedium = sizeof medium/sizeof medium [0];
int lenlarge = sizeof large/sizeof large [0];
cout << endl << max (small, lensmall);
cout << endl << max (medium, lenmedium);
cout << endl << max (large, lenlarge);
cout << endl;
return 0;
}

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

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

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

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

И последнее - мы можем объявить более одного абстрактного типа данных, например:

template.

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

Было бы странно, если бы в C++ существовала возможность создания шаблона функции, но при этом, отсутствовала возможность создания шаблона класса. Естественно, что такая возможность существует - ведь по определению C++ является объектно-ориентированным языком, а основная единица объектно-ориентированного языка это класс.

Приведем пример шаблона класса. Отличие этого примера от предыдущего заключается лишь в том, что теперь массив элементов некоторого абстрактного типа является членом шаблона класса. Для создания приведенного заголовка в Visual Studio вызовите мастер создания класса, укажите, что ваш класс является inline-классом в соответствии с терминологией Microsoft. То есть wizard не будет создавать файла реализации для вашего класса.

#define MAX_INDEX 100
template
class Samples
{
private:
T values [MAX_INDEX]; //Массив для сохранения значений
int size; //Индекс который определяет размер
public:
//Конструктор для инициализации нашего массива
Samples (T vals [], int count)
{
size = count< MAX_INDEX? count: MAX_INDEX;
for (int i=0; ivalues [i] = vals [i];
}

//Конструктор по умолчанию
Samples ()
{
size = 0;
}

//Добавление элемента в массив
bool Add (T& val)
{
if (size < MAX_INDEX)
{
values [size++] = val;
return true;
}
return false;
}

//Функция для получения максимального значения хранящегося в массиве
T Max ()
{
T theMax = values [0];
for (int i=1; iif (values [i]>theMax)
theMax = values [i];
return theMax;
}
};

Для тестирования этого класса подойдет такой код:

#include "stdafx.h"
#include
#include "Samples.h"
using namespace std;
int main ()
{
Samples test;
for (int i = 0; i < 100; i++)
{
test.Add (i);
}
cout << "Maximum value: ";
cout << test.Max () << endl;
return 0;
}

Если вы захотите собрать данный пример на Linux-платформе c помощью компилятора GNU C ++, не забудьте убрать #include "stdafx.h" из *.cpp-файла и директиву препроцессора #pragma once из хедер-файла.

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

Естественно у вас возникает вопрос: как препроцессор узнает, какой тип данных ему необходимо подставлять на место абстрактного? Естественно программист должен указать его сам. Вот как выглядит создание объекта класса, описание которого создается препроцессором на лету из вышеприведенного шаблона класса:

Samples MyData;

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

template
class Samples
{
//...
T Max ();
//...
}

template
T Samples::Max ()
{
T theMax = values [0];
for (int i=1; iif (values [i]>theMax)
theMax = values [i];
return theMax;
}

Однако в этом нет особого смысла, поскольку, если вы захотите разместить определение функции в *.cpp-файле, то получите сообщения об ошибках, источником которых будет линкер. Дело в том, что препроцессор просматривает только *.h-файлы и пространство *.cpp-файла, в котором расположена функция main () программы до точки входа в эту функцию. В некоторых компиляторах он не брезгует также заглянуть за функцию main (). Так что, если вам очень хочется и ваш компилятор это поддерживает, то вы сможете создать определения для функций шаблонного класса после функции main (). Таким образом, препроцессор просто не сможет создать определения функции, не имея к нему доступа. В свою очередь, не найдя для нее точку входа в объектных файлах, линкер сообщит об ошибке.

Иногда можно просто разместить объявление шаблона класса и определения его функций в разные *.h-файлы. Например, так:

#include "Samples.h";
#include "SamplesRealization.h";

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


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=5354