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

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

template <typename Т>

int compare(const T &v1, const T &v2) {

 if (v1 < v2) return -1;

 if (v2 < v1) return 1;

 return 0;

}

Определение шаблона начинается с ключевого слова template, за которым следует разделяемый запятыми и заключенный в угловые скобки (<>) список параметров шаблона (template parameter list), один или несколько параметров шаблона (template parameter).

Список параметров в определении шаблона не может быть пустым

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

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

Например, рассматриваемая функция compare() объявляет единственный параметр типа Т. В шаблоне compare имя Т можно использовать там, где должно быть название типа данных. Фактический тип Т будет определен компилятором на основании способа применения функции.

Создание экземпляра шаблона функции

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

cout << compare(1, 0) << endl; // Т - тип int

Здесь аргумент имеет тип int. Компилятор выведет и использует тип int как аргумент шаблона, а также свяжет этот аргумент с параметром Т шаблона.

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

// создание экземпляра int compare(const int&, const int&)

cout << compare(1, 0) << endl; // T - тип int

// создание

// экземпляра int compare(const vector<int>&, const vector<int>&)

vector<int> vec1{1, 2, 3}, vec2{4, 5, 6};

cout << compare(vec1, vec2) << endl; // T - тип vector<int>

Здесь компилятор создает два экземпляра разных версий функции compare(). В первой из них параметр Т заменен типом int.

int compare(const int &v1, const int &v2) {

 if (v1 < v2) return -1;

 if (v2 < v1) return 1;

 return 0;

}

Во втором вызове создается версия функции compare() с параметром Т, замененным типом vector<int>. Такое создание компилятором функций обычно и называют созданием экземпляра шаблона.

Параметры типа шаблона

У функции compare() есть один параметр типа (type parameter) шаблона. Как правило, параметр типа можно использовать как спецификатор типа таким же образом, как и встроенный спецификатор типа или класса. В частности, параметр типа применим при назначении типа возвращаемого значения или типа параметра функции, а также в объявлениях переменных или приведениях в теле функции:

// ok: для возвращаемого значения и параметра используется тот же тип

template <typename Т> Т foo(Т* p) {

 Т tmp = *p; // тип tmp совпадает с типом, на который указывает p

 // ...

 return tmp;

}

Каждому параметру типа должно предшествовать ключевое слово class или typename:

// ошибка: U должно предшествовать либо typename, либо class

template <typename Т, U> Т calc(const T&, const U&);

В списке параметров шаблона эти ключевые слова имеют одинаковый смысл и применяются взаимозаменяемо. Оба ключевых слова применимы одновременно:

// ok: в списке параметров шаблона нет никакой разницы между ключевыми

// словами typename и class

template <typename Т, class U> calc(const T&, const U&);

Для обозначения параметра типа шаблона интуитивно понятней использовать ключевое слово typename, а не class; в конце концов, для фактического типа параметра вполне может быть использован встроенный тип, а не только класс. Кроме того, ключевое слово typename более точно указывает на то, что следующее за ним имя принадлежит типу. Однако ключевое слово typename было добавлено в язык С++ как часть стандарта С++, поэтому в устаревших программах, вероятнее всего, осталось исключительно ключевое слово class.

Параметры значения шаблона

Кроме параметров типа, в определении шаблона могут быть использованы параметры значения (nontype parameter). Параметр значения представляет значение, а не тип. При определении параметров значения вместо ключевого слова class или typename используются имена типов.

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

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

template<unsigned N, unsigned M>

int compare(const char (&p1)[N], const char (&p2)[M]) {

 return strcmp(p1, p2);

}

При вызове следующей версии функции compare() компилятор будет использовать размер литералов для создания экземпляра шаблона с размерами, которыми заменяют параметры N и M:

compare("hi", "mom")

Не забывайте, что компилятор завершает строковый литерал пустым символом (см. раздел 2.1.3). В результате компилятор создаст такой экземпляр:

int compare(const char (&p1)[3], const char (&p2)[4])

Параметр значения может быть целочисленным типом, указателем, ссылкой на объект (l-значением) или на тип функции. Аргумент, связанный с целочисленным параметром значения, должен быть константным выражением. У аргументов, привязанных к указателю или ссылочному параметру значения, должна быть статическая продолжительность существования (см. главу 12). Нельзя использовать обычный (нестатический) локальный или динамический объект как аргумент шаблона для параметра значения шаблона в виде ссылки или указателя. Параметр-указатель может быть также создан как nullptr или нулевое константное выражение.

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

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

Шаблоны функции со спецификаторами inline и constexpr

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

// ok: спецификатор inline следует за списком параметров шаблона

template <typename Т> inline Т min(const Т&, const Т&);

// ошибка: неправильное размещение спецификатора inline

inline template <typename T> T min(const T&, const T&);

Создание кода, независимого от типа

Продемонстрируем два наиболее важных принципа создания обобщенного кода на примере функции compare().

• Параметры функций в шаблоне должны быть ссылками на константу.

• При проверке в теле шаблона следует использовать только оператор сравнения <.

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

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

// ожидаемое сравнение

if (v1 < v2) return -1;

if (v1 > v2) return 1;

return 0;

Однако написание кода, использующего только оператор <, снизит требования к типам, которые применимы в функции compare(). Эти типы должны поддерживать оператор <, но не обязаны поддерживать оператор >.

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

// версия функции compare(), корректно работающая даже с

// указателями; см. p. 14.8.2

template <typename Т> int compare(const T &v1, const T &v2) {

 if (less<T>()(v1, v2)) return -1;

 if (less<T>()(v2, v1)) return 1;

 return 0;

}

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

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

Компиляция шаблона

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

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

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

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

Ключевая концепция. Шаблоны и заголовки

Шаблоны содержат два вида имен:

• не зависящие от параметров шаблона;

• зависящие от параметров шаблона.

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

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

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

Ошибки компиляции проявляются, главным образом, во время создания экземпляра

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

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

Второй этап обнаружения ошибок — когда компилятор встречает применение шаблона. На данном этапе компилятор также способен проверить немногое. Для вызова шаблона функции компилятор обычно проверяя количество и типы аргументов. Он может также проверить совпадение типов двух аргументов. Для шаблона класса компилятор может проверить количество и правильность предоставленных аргументов шаблона, но не более.

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

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

if (v1 < v2) return -1; // для объектов типа Т требуется оператор <

if (v2 < v1) return 1;  // для объектов типа Т требуется оператор <

return 0;               // возвращает int; не зависит от Т

Когда компилятор обрабатывает тело этого шаблона, он не может проверить корректность условий в операторах if. Если переданные функции compare() аргументы имеют оператор <, то код сработает прекрасно, но не в противном случае. Например:

Sales_data data1, data2;

cout << compare(data1, data2) << endl; // ошибка: у Sales_data нет

                                       // оператора <

Этот вызов создает экземпляр функции compare() с параметром Т, замененным классом Sales_data. Если условия попытаются использовать оператор < для объектов класса Sales_data, то окажется, что такого оператора нет. В результате получится экземпляр функции, которая не будет откомпилирована. Такие ошибки, как эта, не могут быть обнаружены, пока компилятор не создаст экземпляр определения функции compare() для типа Sales_data.

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

Упражнения раздела 16.1.1

Упражнение 16.1. Определите создание экземпляра.

Упражнение 16.2. Напишите и проверьте собственные версии функций compare().

Упражнение 16.3. Вызовите собственную функцию compare() для объекта класса Sales_data и посмотрите, как ваш компилятор обрабатывает ошибки во время создания экземпляра.

Упражнение 16.4. Напишите шаблон, действующий как библиотечный алгоритм find(). Функция будет нуждаться в двух параметрах типа шаблона: один — для представления параметров-итераторов функции и другой — для типа значения. Используйте свою функцию для поиска заданного значение в векторе vector<int> и списке list<string>.

Упражнение 16.5. Напишите шаблон функции print() из раздела 6.2.4, которая получает ссылку на массив и может обрабатывать массивы любого размера и любого типа элементов.

Упражнение 16.6. Как работают библиотечные функции begin() и end(), получающие аргумент в виде массива? Определите собственные версии этих функций.

Упражнение 16.7. Напишите шаблон constexpr, возвращающий размер заданного массива.

Упражнение 16.8. В разделе "Ключевая концепция" в разделе 3.4.1 упоминалось о том, что программисты С++ привыкли использовать оператор !=, а не <. Объясните причину этой привычки.