Template class t c что это
Перейти к содержимому

Template class t c что это

Просто о шаблонах C++

Статья написана с целью максимально просто, на живых примерах рассказать о шаблонах C++.

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

Статья пытается ответить на все эти и многие другие вопросы.

Вступление

Для того чтобы статья читалась с большей пользой, по желанию можно ознакомиться с несколькими ремарками:

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

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

Стоит компилировать примеры. В идеале, экспериментировать: пробовать менять и улучшать код. Как любая другая тема в программировании, шаблоны лучше всего познаются практикой. Если лень разбираться с настройкой среды разработки , можно использовать какой-нибудь онлайн-компилятор. Например, для анализа ассемблерного кода, получаемого после компиляции, в статье использовался онлайн-компилятор godbolt.org (с отключением оптимизаций опцией «-O0»).

Вопреки традиции учебных материалов, примеры кода не содержат распечатки переменных в поток вывода (без «printf» / «std::cout»). Это было сделано намеренно, чтобы избежать лишнего шума в коде. Если будете компилировать код примеров в IDE, можете просматривать значения переменных в дебаггере. Если же удобнее использовать поток вывода — как вариант, можно использовать следующий макрос:

Цель статьи — рассказать про шаблоны максимально понятно, чтобы пользу от чтения извлёк даже начинающий программист. С позиций бывалого разработчика исходный код примеров далёк от идеала: почти нет проверок на корректность значений переменных, для индексов используется тип «int» вместо «size_t», местами дублируется код, почти не используется передача значений по ссылкам и т.д. Это делалось чтобы минимально уходить в смежные темы, концентируясь, в первую очередь, на иллюстрации использования шаблонов.

Для иллюстрации приёмов на рабочем коде уходить в не связанные с шаблонами темы иногда всё-таки приходилось. Комментарии, не относящиеся напрямую к теме шаблонов, помечены звёздочкой — вот так: (*). В случае, если при прочтении больше интересует тема шаблонов, — такие комментарии можно не читать.

Хабр — преимущественно русскоязычный ресурс. Поэтому я старался писать статью на русском. Как часто бывает в программировании, при этом были трудности с переводом терминов. Например, для понятия «template instantiation» используется несколько «творческий» перевод «порождение шаблона». Неуклюже — однако лучшего перевода придумать не вышло. Чтобы компенсировать возможные непонятки, к определениям терминов привязаны оригинальные названия, которые можно посмотреть наведя мышку . Если вы знаете варианты, которые будут удачнее приведённых в статье, — пишите, обсудим. Я с радостью поменяю терминологию на более распространённую.

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

Оглавление

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

Концепция шаблонов возникла из принципа программирования Don’t repeat yourself. Можно проследить логику, по которой авторы C++ ввели шаблоны в язык.

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

…переписывают, убирая логику в функцию:

Использование функций даёт несколько преимуществ:

Если надо поменять повторяющуюся логику — достаточно сделать это в функции, не надо менять все копии одинакового кода в программе. Если бы в примере выше вариант без функции содержал системную ошибку в тернарных вызовах, с путаницей порядка операндов: «(a >= b) ? b : a» и «(max_ab >= c) ? c : max_ab» — ошибку пришлось бы искать и править во всех местах использования. Вариант с функцией же требует одной правки — в реализации функции.

При грамотном именовании в коде с функциями логика кода становится прозрачнее. В примере без функции внимательного прочтения требует каждая конструкция вида «(. >= . ) ? . : . » , надо узнавать повторяющуюся логику выбора большего значения из двух каждый раз заново. Функция же во втором варианте именует повторяющуюся логику, за счёт чего общий смысл программы понятнее.

Процедурное программирование делает код чище. Однако, что если логику получения максимального элемента надо поддерживать для всех числовых типов: для всех размеров (1, 2, 4, 8 байт), как знаковых, так и беззнаковых (signed / unsigned), для чисел с плавающей точкой («float», «double»)?

Можно воспользоваться перегрузкой функций:

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

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

Перегрузки функций с повторяющийся логикой заменились на одну «функцию» с новой конструкцией — template<typename Type>. Слово «функция» взято тут в кавычки намеренно. Это не совсем функция. Данная запись означает для компилятора следующее: «После конструкции template<typename Type> описан шаблон функции, по которому подстановкой типа вместо шаблонного аргумента Type порождаются конкретные функции».

Не стоит путать при этом аргументы функции (в примере — это «Type a» и «Type b») и аргументы шаблона (в примере — это «typename Type»). Первые задают значения, которые принимает функция при вызове. Вторые же задают параметры, подстановкой в которые значений по месту использования порождаются конкретные функции из шаблонов.

Использование шаблона выглядит так: «max<int>(a, b)«. В треугольных скобках передаются значения шаблонных аргументов. В данном случае, в качестве значения шаблонного аргумента «Type» передаётся значение — тип «int». После подстановки компилятор создаст «под капотом» конкретную функцию из обобщённого кода. То, что вызывается по записи «max<int>()», для компилятора выглядит так:

Встречая дальше обращения к шаблонной функции с подстановкой в качестве «Type» типа «int», компилятор будет использовать эту же сгенерированную из шаблона функцию.

Встретив же следующую запись — «max<char>(aChar, bChar)» — компилятор породит для себя новую функцию — но по тому же шаблону:

Несмотря на родство по шаблону, функции «max<int>()» и «max<char>()» — совершенно самостоятельны, каждая из них будет превращаться при компиляции в свой ассемблерный код.

Зафиксируем также терминологию более высокого уровня.

2. Выведение типов шаблонных аргументов

В примере с шаблоном функции «template<Type> max(Type, Type)» использовалась явная передача типов в шаблон. Однако во многих случаях компилятор может автоматически вывести тип шаблонного аргумента.

Вызов шаблонной функции из примера.

. можно записать, опустив <int>:

Такая запись корректна с точки зрения языка. Компилятор проанализирует типы переменных «a» и «b» и выполнит выведение типа для передачи в качестве значения шаблонного аргумента «Type».

Тип переменной «a» — «int», тип переменной «b» – тоже «int». Они передаются в шаблон функции «template<Type> Type max(Type, Type)», в котором ожидается, что оба аргумента будут иметь одинаковый тип «Type». Так как типы «a» и «b» совпадают, и нет других правил ограничивающих данный шаблонный аргумент «Type», компилятор делает вывод, что записью «max(a, b)» ожидают применения шаблонной функции «max<int>(a, b)».

Стоит отметить, что, например, следующий код.

. не скомпилируется с ошибкой вроде: «deduced conflicting types for parameter ‘Type’».

Проблема в том, что для этого кода типы переменных «a» и «b» не совпадают. Компилятор не может однозначно определить какой тип надо передать в качестве значения аргумента «Type». У него есть вариант подставить тип «int» или тип «char». Непонятно какая из подстановок ожидается программистом.

Чтобы избавиться от этой проблемы, можно применить явную передачу типа в шаблон:

Теперь всё хорошо. Шаблонная функция определена однозначно: «int max<int>(int, int)». Значение переменной «bChar» в этом вызове приведётся к типу «int» — так же, как это произошло бы при вызове нешаблонной функции «int max(int, int)» из самого начала статьи.

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

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

Вот, например, описание шаблонного класса Interval. С его помощью можно описывать промежутки значений произвольного типа:

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

Пример использование шаблона класса «template<Type> class Interval»:

Встретив запись «Interval<int>» в первый раз, по шаблону класса будет порождён новый шаблонный класс. Порождённый класс будет выглядеть для компилятора следующим образом:

Так же, как это было с функциями, порождение шаблонного класса выполнится подстановкой «int» вместо «Type». Порождённый тип будет использоваться везде, где шаблон «template<Type> class Interval<Type>» с подстановкой «int».

4. Специализации

Это, пожалуй, один из самых важных и сложных разделов статьи, поэтому он будет длиннее других.

Лучший пример на котором можно разобраться со специализациями шаблонов — шаблон класса «массив». Вспомним, массив – структура данных, хранящая набор однотипных значений последовательно одно за другим в памяти. В стандартной библиотеке шаблонов эту структуру данных реализует шаблон класса «std::vector<>».

Вот элементарная реализация шаблона массива:

По реализации, надеюсь, всё понятно. Рассмотрим пример использования:

«SimpleArray<int>» — шаблонный класс, для получения которого в шаблон «template<Type> class SimpleArray» в качестве аргумента «Type» передаётся тип «int». Массив заполняется с помощью обращения к методу «setElement()», после чего в цикле рассчитывается сумма всех элементов.

Это рабочий шаблон. Однако есть ситуация, в которой он не достаточно эффективен. Вот пример использования шаблонного класса с подстановкой типа bool:

Элементы массива имеют булевый тип, который выражается одним из всего двух возможных значений: «false» или «true» (численно описывающихся, соответственно, значениями «0» или «1»). Вот как «SimpleArray<bool>» использует память для хранения элементов (тут исходим из того, что тип «bool» занимает один байт):

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

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

Вот по какому принципу описывается специализация:

Ниже — полный код специализации шаблона класса «template<Type> class SimpleArray».

(*) Ликбез по побитным операциям

Для доступа к битам используются следующие побитовые операции:

Операция побитового сдвига влево (<<)

Операция побитового «И» (&)

Операция побитового «ИЛИ» (|)

Операция побитового отрицания (

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

Как получить значение бита с заданым индексом? Значение бита может быть либо «0», либо «1», поэтому для его выражения используют тип «bool». «bool» имеет смысл «ложь» если все его биты равны «0» и смысл «истина» если хотя бы один его бит не равен «0». Таким образом, чтобы понять имеет ли интересующий нас бит значение «0» или «1», надо добиться того чтобы все биты кроме интересующего нас приняли значение «0». Для этого используются так называемые битовые маски — значения которыми «фильтруются» интересующие нас биты.

Например, надо получить значение бита с индексом «4». Для того чтобы «обнулить» значения всех битов кроме интересующего, формируется битвая маска в которой бит по индексу «4» имеет значение «1», а все остальные биты — значение «0». После этого, выполнив побитовое «И» каждого бита значения с битами маски можно добиться того чтобы все биты кроме интересующего гарантированно стали равны «0»:

Получение бита 4

Получение бита 0

Разберём обобщённый алгоритм. Как понятно из примеров, чтобы получить значение бита по индексу «bitIndex», надо выполнить операцию побитового «И» между значением и маской, в которой бит по индексу «bitIndex» имеет значение «1», а остальные биты — значение «0». В коде эта логика записывается следующим образом:

Как читать биты терерь известно.

Однако, как заполнить бит в байте по индексу нужным значением? Эту операцию лучше всего выполнять в два этапа:

Значение нужного бита в байте «сбрасывается» в «0». Этого добиваются выполняя логическое «И» между изменяемым байтом и маской в которой бит по целевому индексу имеет значение «0», а все остальные биты — значение «1».

Сброшенное в «0» значение нужного бита «записываются» нужным значением. Это достигается выполнением логического «ИЛИ» между результатом первого этапа и маской в которой по целевому индексу находится значение «1», а все остальные биты имеют значение «0».

Звучит сложно. Чтобы понять как это работает проще всего будет рассмотреть несколько примеров (в скобках записывается с какого на какое значение бита происходит изменение):

Заполнение бита 2 значением 1 (0 -> 1)

В коде эта логика записывается следующим образом (конкретные значения взяты из первого примера с объяснением выставления полей):

Рассмотрим новый пример использования «template<Type> class SimpleArray» с поддержкой специализации по типу «bool»:

Заканчивая раздел, чуть заглянем в будущее. Изначально специализации создавались как механизм для выбора оптимальной реализации шаблона для конкретного типа. Однако позже они начали исполнять одну из ключевых ролей в метапрограммировании на C++. Возможность выбирать вариации шаблона по передаваемому типу позволяет анализировать типы в программе, что открывает доступ к рефлексии времени компиляции.

5. Валидация шаблонных аргументов

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

Что ж. Добавим немного дёгтя.

Уже при описании шаблонной функции «template<Type> max(Type, Type)» неминуемо возникал вопрос: как проверяется корректность типа, который подставляется в шаблон? Ведь в шаблоне тип как-то используется. Например, что будет если передать в качестве аргумента «template<Type> max(Type, Type)» тип, не поддерживающий оператор «> cpp»>template<typename Type> Type max(Type a, Type b) < return (a >= b ? a : b); > // Структура, определяющая позицию точки в двухмерном пространстве. Для точки // нельзя сказать «больше» ли она другой точки. Можно сравнивать конкретные // координаты («x» или «y») точек, но нельзя сравнить сами точки. Для структуры // Point2D _не определена_ операция сравнения «> Point2D» в аргумент «Type» шаблона // «template<Type> max(Type, Type)» породится шаблонная функция, которая для // компилятора выглядит так: // // Point2D max<Point2D>(Point2D a, Point2D b) // < // return (a >= b ? a : b); // > // // В теле функции выполняется сравнение двух значений («a» и «b») имеющих тип // «Point2D». Однако, как было отмечено выше, для их типа «Point2D» операция // сравнения _не определена_ . Компилятору остаётся лишь сгенерировать ошибку // компиляции вроде следующей (так отображает ошибку компилятор GCC): // // «no match for ‘operator<=’ (operand types are ‘Point2D’ and ‘Point2D’)» return 0; >

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

Сейчас в промышленно используемом C++ нет механизма валидации шаблонных аргументов.

Долгое время это была одна из главных проблем шаблонов и, в целом, одной из главных проблем языка C++. Особенно ужасно она проявляла себя в сложных шаблонных конструкциях из сторонних библиотек. Там ошибки компиляции могли появляться в глубинах логики чужих шаблонов. Приходилось долго разбираться в реализации стороннего кода. Имевшие дело со стандартной библиотекой шаблонов , с её самыми популярными шаблонами классов «std::vector<>» и «std::map<>», наверняка не раз страдали от многоэтажных ошибок компиляции в недрах их реализаций.

Проблему с валидацией решали по-разному. Использовали свойства подстановок, вводили в язык конструкцию «static_assert()», придумывали стили комментариев, в которых текстом описывались бы требования к аргументам шаблонов.

Лишь спустя годы поисков, к версии C++20 комитет по стандартизации языка прекратил хождение по мукам и наконец-то качественно решил вопрос, введя в язык КОНЦЕПТЫ .

Концепты позволяют описывать требования к типу, который передаётся как шаблонный аргумент. Например, для шаблона функции «template<Type> Type max(Type, Type)» с помощью концептов можно потребовать передавать в качестве значения «Type» тип, поддерживающий операцию сравнения. С помощью концептов компилятор может обнаружить ошибку до выполнения некорректной подстановки типа в шаблон.

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

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

На этом закончу пропаганду. Цель статьи — дать вводную начинающим разработчикам, которым предстоит работать с реальным кодом, использующимся в индустрии прямо сейчас. Код этот, увы, написан, в основном, с использованием старых стандартов. Освоим для начала их.

6. Больше шаблонных аргументов

До этого речь шла о шаблонах зависящих от одного аргумента. Но C++ позволяет задавать и большее их количество. Чтобы прочувствовать как это используется в реальном коде, рассмотрим шаблон, зависящий от двух шаблонных аргументов.

Ниже представлена элементарная реализация словаря. Использование шаблона позволяет как ключ, так и значение задавать произвольным типом:

Пример, иллюстрирующий использование шаблона:

К сожалению, тема шаблонов от произвольного количества аргументов слишком обширная. В данной статье мы её касаться не будем. Если когда-нибудь напишу материал по теме — обязательно оставлю здесь ссылку на него.

7. Шаблонные аргументы-константы

До этого рассматривались шаблоны, принимающие лишь типы в качестве шаблонных аргументов. Однако в качестве аргументов шаблонов могут выступать также константы времени компиляции. Такие аргументы по-английски называются non-type template arguments, дословно «шаблонные аргументы не являющиеся типами». Дословный перевод по-русски звучит неуклюже, поэтому дальше будем использовать термин «шаблонные аргументы-константы «.

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

За счёт того, что значение шаблонного аргумента-константы по определению не зависит от вычислений этапа исполнения программы, компилятор с большой вероятностью сможет оптимизировать код при компиляции, подставив в ассемблерном коде константу 4*3*2*1 (то есть, сразу значение 24), вместо полноценного вызова функции «getFactorial<4>()» и всей содержащейся в ней логики.

Рассмотрим какие ещё варианты передачи значения шаблонного аргумента-константы допустимы:

В разделе «Частичные специализации шаблонов» будет ещё один пример, использующий шаблонные аргументы-константы. Он ближе к реальной жизни.

8. Передача шаблонных аргументов в шаблонном контексте

Вероятно, в разделе про шаблонные классы у читающего мог возникнуть резонный вопрос: можно ли передать шаблонный класс в функцию, сохранив код обобщённым? Например, возможно ли описать функцию для получениея максимального элемента в шаблонном массиве «template<Type> SimpleArray».

Можно начать плодить перегрузки с конкретными шаблонными классами:

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

Что ж, C++ позволяет использовать шаблон и в такой ситуации. На самом деле, случаи нужного нам типа подстановок встречались в статье раньше, просто внимание на них не акцентировалось. Вот, к примеру, метод шаблона «template<Type> class Interval»:

Вместо повторяющихся перегрузок «getMaxElement()», можно описать шаблон функции, аргумент которой передаётся в шаблон класса «template<Type> class SimpleArray»:

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

9. Частичные специализации шаблонов

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

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

В шаблоне класса «template<Type> SimpleArray» использовалась динамическая память:

Использование динамической памяти позволяло создавать массивы разной длины, определяемой на этапе исполнения программы:

Память для элементов выделяется единожды, при создании экземпляров. После этого расширить или сократить объём памяти нельзя. Так ли важна эта возможность?

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

Иллюстрация принципа работы стека и динамической памяти

Картинка, иллюстрирующая принцип работы кучи и стека. Цветные элементы со знаками «+» и «-» иллюстрируют принцип по которым работает, соответственно, выделение и освобождение памяти этих типов.

Блоки памяти в стеке выделяются простым сдвигом вершины стека.

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

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

Можно сравнить ассемблерный код который получится при компиляции примера:

Уже по количеству команд для записи значений элементов видно что использование динамической памяти требует большего количества действий. Однако «call» вызовы для создания и освобождения динамической памяти — это ещё более тяжёлые операции обращения к функциям.

Было бы здорово получить структуру данных, хранящую элементы в стековой памяти. В стандартной библиотеке шаблонов такую структуру реализует шаблон «std::array<>».

Чтобы подобную структуру данных получить из «template<Type> SimpleArray», надо сменить тип поля для хранения элементов массива:

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

Вот полная реализация обобщённого шаблона класса. Она очень простая:

Теперь внимательному читателю, вероятно, интересно: что же будет со специализацией по типу «bool»? Она, с одной стороны, требует «фиксации» значения первого шаблонного аргумента «Type», с другой — должна поддерживать произвольное значение второго аргумента «Size» (массив флагов может быть любой длины).

Для решения этого вопроса существуют частичные специализации шаблонов :

Рассмотрим пример использования, аналогичный примеру из четвёртого раздела, разобрав логику по которой компилятор будет выбирать специализацию:

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

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

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

Заключение

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

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

Updated: Если вас заинтересовала тема шаблонов в C++, уже сейчас есть хорошая статья от @4eyes, рассматривающая более продвинутые техники метапрограммирования на практическом примере: «О шаблонах чуть сложнее».

1. Вопрос: Чем шаблоны отличаются от макросов?

Ответ: Макросы выполняют текстовую подстановку аргументов, в то время как шаблоны лексически и синтаксически проверяются компилятором. Если для решения задачи стоит выбор между шаблонами и макросы — стоит предпочитать шаблоны.

Эти вопросы логично возникают у программистов, изучающих шаблоны. Действительно, на первый взгляд, шаблоны похожи на макроподстановки компиляторов. Оба механизма позволяют «генерировать» код по определённому трафарету с подстановкой передаваемых по месту применения значений-аргументов.

Главная разница заключается в том, что макросы — это действия с текстом, который не воспринимается компилятором как исходный код состоящий из определения функций, переменных, выражений, и т.д. С текстом программы работает препроцессор, для которого программа — набор символов (букв, цифр, знаков для операторов, пробелов, и т.д.), которые можно копировать и вставлять полностью аналогично тому как программист это делает в IDE с помощью Ctrl+C, Ctrl+V:

#include — указание «вставить вместо макроса весь текст содержащийся в файле»

#define — указание «встречая идентификатор определяющий макрос, вставить текст следующий за макросом с заменой аргументов передаваемым по месту использования текстом».

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

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

Допустим, имеется следующие фукнции для работы с файлами, содержащими числа:

Задача следующая — надо найти максимальное число в файле.

Для начала рассмотрим как в этой задаче сработает обобщённая логика для поиска максимального значения, описанная с помощью макроса:

На первый взгляд, логика должна работать корректно.

Однако давайте посмотрим в какой код буквально раскроется строчка с макросом:

Именно с таким кодом функция «main()» отправится на компиляцию. Если обратить внимание на то как работает функция «loadNextValueFromFile()» и внимательно вчитаться в то что сгенерировал препроцессор, в программе можно увидеть неприятный баг.

Вот как выполнится логика алгоритма если в файле содержатся числа «1, 3, 0»:

Записываем в currentMax первое число из файла (число «1»).

Для расчёта значения по условию «false» снова вызывается «loadNextValueFromFile(«file»)». Этот вызов вернёт число «0», так как каретка передвинулась при вычислении сравнения. В currentMax записывается число «0», которое, очевидно, не является самым большим в файле.

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

Из-за примитивности механизма работы препроцессора, использование макросов в прикладном коде будет стабильно приводить к подобным трудно уловимым ошибкам.

Поэтому лучше предпочитать макросам шаблоны:

«max<int>()» — это простой вызов функции. При вызове функций выражения, передаваемые в качестве аргументов, вычисляются единожды перед передачей в функцию. Ошибки компиляции внутри шаблонной функции будут проверяться компилятором на уровне логических конструкций программы, без «магического» собирания текста программы из кусков.

2. Вопрос: Увеличивают ли шаблоны объём скомпилированного кода?

Ответ: Относительно эквивалентной логики без шаблонов — нет.

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

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

Сравнение ассемблерного кода для функции «max()»

Код без шаблонов:

Код с шаблонами:

Сравнение ассемблерного кода:

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

Сравнение ассемблерного кода для метода «Interval::intersection()»

Код с шаблонами

Сравнение ассемблерного кода:

3. Вопрос: Увеличивают ли шаблоны расход ресурсов на компиляцию кода?

Ответ: Да, но после перехода на C++20 ситуация может стать лучше.

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

Концепты из нового стандарта C++20 могут поменять ситуацию — по крайней мере, со временем выполнения компиляции. Они позволяют останавливать подстановку аргументов в шаблон до полноценного формирования шаблонного типа, и экономить таким образом время на завершение генерации априори некорректного типа.

4. Вопрос: Затрудняют ли шаблоны отладку кода?

Ответ: Поиск ошибок компиляции — затрудняют (однако с приходом концептов из C++20 станет лучше). Отладку ошибок в логике исполнения программы — нет.

Проблемы разбора ошибок компиляции были подробно описаны в пятом разделе.

A template is a cookie-cutter that specifies how to cut cookies that all look pretty much the same (although the cookies can be made of various kinds of dough, they’ll all have the same basic shape). In the same way, a class template is a cookie cutter for a description of how to build a family of classes that all look basically the same, and a function template describes how to build a family of similar looking functions.

Class templates are often used to build type safe containers (although this only scratches the surface for how they can be used).

What’s the syntax / semantics for a “class template”?

Consider a container class Array that acts like an array of integers:

Repeating the above over and over for Array of float , of char , of std::string , of Array-of- std::string , etc, would become tedious. Instead, you add the template<typename T> before the class definition (the T can be any identifier you want, T is just the most commonly used one, especially in examples). Then, instead of using int or float or char where referring to the data type, you use T instead. Also, instead of just referring to the class as Array, it’s Array<T> when referring to the template, or Array<int> , Array<float> , etc. when referring to a specific instantiation.

Just as with a normal class, you can optionally define your methods outside the class:

Unlike template functions, template classes (instantiations of class templates) need to be explicit about the parameters over which they are instantiating:

Note that prior to C++11, a space was required between the two > ’s in the last example. Without this space, the C++98/C++03 compiler would see a >> (right-shift) token instead of two > ’s. Aren’t you lucky that it is no longer the case in C++11?

What’s the syntax / semantics for a “function template”?

Consider this function that swaps its two integer arguments:

If we also had to swap floats, longs, Strings, Sets, and FileSystems, we’d get pretty tired of coding lines that look almost identical except for the type. Mindless repetition is an ideal job for a computer, hence a function template:

Every time we used swap() with a given pair of types, the compiler will go to the above definition and will create yet another “template function” as an instantiation of the above. Unlike template classes, template functions usually do not need to be explicit about the parameters over which they are instantiating. The compiler can usually determine them automatically. E.g.,

Note: A “template function” is the instantiation of a “function template”.

How do I explicitly select which version of a function template should get called?

When you call a function template, the compiler tries to deduce the template type. Most of the time it can do that successfully, but every once in a while you may want to help the compiler deduce the right type — either because it cannot deduce the type at all, or perhaps because it would deduce the wrong type.

For example, you might be calling a function template that doesn’t have any parameters of its template argument types, or you might want to force the compiler to do certain promotions on the arguments before selecting the correct function template. In these cases you’ll need to explicitly tell the compiler which instantiation of the function template should be called.

Here is a sample function template where the template parameter T does not appear in the function’s parameter list. In this case the compiler cannot deduce the template parameter types when the function is called.

To call this function with T being an int or a std::string , you could say:

Here is another function whose template parameters appear in the function’s list of formal parameters (that is, the compiler can deduce the template type from the actual arguments):

Now if you want to force the actual arguments to be promoted before the compiler deduces the template type, you can use the above technique. E.g., if you simply called g(42) you would get g<int>(42) , but if you wanted to pass 42 to g<long>() , you could say this: g<long>(42) . (Of course you could also promote the parameter explicitly, such as either g(long(42)) or even g(42L) , but that ruins the example.)

Similarly if you said g(«xyz») you’d end up calling g<char*>(char*) , but if you wanted to call the std::string version of g<>() you could say g<std::string>(«xyz») . (Again you could also promote the argument, such as g(std::string(«xyz»)) , but that’s another story.)

Another time when you must specify the types is when the function takes two parameters of the same type, but you give it two different types.

Since m and n have different types, the compiler can’t deduce what type to use for T , so you have to tell it what to use:

What is a “parameterized type”?

Another way to say, “class templates.”

A parameterized type is a type that is parameterized over another type or some value. List<int> is a type ( List ) parameterized over another type ( int ).

What is “genericity”?

Yet another way to say, “class templates.”

Not to be confused with “generality” (which just means avoiding solutions which are overly specific), “genericity” means class templates.

My template function does something special when the template type T is int or std::string ; how do I write my template so it uses the special code when T is one of those specific types?

Before showing how to do this, let’s make sure you’re not shooting yourself in the foot. Does the function’s behavior appear different to your users? In other words, is the observable behavior different in some substantive way? If so, you’re probably shooting yourself in the foot and you will probably confuse your users — you’re probably better off using different functions with different names — don’t use templates, don’t use overloading. For example, if the code for int inserts something into a container and sorts the result, but the code for std::string removes something from a container and does not sort the result, those two functions ought not to be an overloaded pair — their observable behavior is different so they ought to have different names.

However if the function’s observable behavior is consistent for all the T types with the differences limited to implementation details, then you can proceed. Let’s proceed with an example of this (conceptual only; not C++):

One way to implement the above is via template specialization. Instead of a switch -statement, you end up breaking up the code into separate functions. The first function is the default case — the code to be used when T is anything other than int or std::string :

Next are the two specializations, first for the int case…

…and next for the std::string case…

That’s it; you’re done. The compiler will automagically select the correct specialization when it sees which T you are using.

Huh? Can you provide an example of template specialization that doesn’t use foo and bar ?

One of several ways I personally use template specialization is for stringification. I usually use a template to stringify various objects of various types, but I often need to specialize the code for stringifying certain specific types. For instance, when stringifying bool s I prefer «true» and «false» over «1» and «0» so I use std::boolalpha when T is bool . Also I often prefer floating point output to contain all the digits (so I can see very small differences, etc.) so I use std::setprecision when T is a floating point type. The end result usually looks something like this:

Conceptually they all do the same thing: stringify the parameter. That means the observable behavior is consistent, therefore the specializations do not confuse callers. However the details for implementing that observable behavior is slightly different for bool and floating point types, so template specialization is a good approach.

But most of the code in my template function is the same; is there some way to get the benefits of template specialization without duplicating all that source code?

For example, suppose your template function has a bunch of common code along with a relatively small amount of T -specific code (conceptual only; not C++):

If you blindly applied the advice from the FAQ on template specialization, you would end up duplicating all that code before and after the pseudo-switch statement. The way to get the best of both worlds — to get the benefits of T -specific pieces without duplicating the entire function, is to extract the pseudo-switch statement portion into a separate function foo_part() , and use template specialization on that separate function:

The main foo() function would be a simple template — no specializations. Note that the pseudo-switch statement has been replaced by a call to foo_part() :

As you can see, the body of foo() now doesn’t mention any particular T . It all gets figured out automatically. The compiler generates foo for you based on type T , and will generate the correctly typed foo_part function based on the actual compile-time known type of the x argument. Proper specializations of foo_part will be instantiated.

All those templates and template specializations must slow down my program, right?

This is a quality-of-implementation issue so your results may vary. However there is usually no slow-down at all. If anything, the templates might affect the speed of compilation slightly, but once the types are resolved by the compiler at compile-time, it will typically generate code that is just as fast as with non-template functions, including inline-expanding appropriate functions, etc.

So templates are overloading, right?

Function templates participate in name resolution for overloaded functions, but the rules are different. For a template to be considered in overload resolution, the type has to match exactly. If the types do not match exactly, the conversions are not considered and the template is simply dropped from the set of viable functions. That’s what is known as “SFINAE” — Substitution Failure Is Not An Error. Example:

In this example, foo<T> cannot be considered for the first or the second call to foo in the body of main because neither 42 nor 42.0 gives the compiler any information to deduce T . The third call, however, includes foo<T> with T = char and it wins.

Why can’t I separate the definition of my templates class from its declaration and put it inside a .cpp file?

If all you want to know is how to fix this situation, read the next two FAQs. But in order to understand why things are the way they are, first accept these facts:

  1. A template is not a class or a function. A template is a “pattern” that the compiler uses to generate a family of classes or functions.
  2. In order for the compiler to generate the code, it must see both the template definition (not just declaration) and the specific types/whatever used to “fill in” the template. For example, if you’re trying to use a Foo<int> , the compiler must see both the Foo template and the fact that you’re trying to make a specific Foo<int> .
  3. Your compiler probably doesn’t remember the details of one .cpp file while it is compiling another .cpp file. It could, but most do not and if you are reading this FAQ, it almost definitely does not. BTW this is called the “separate compilation model.”

Now based on those facts, here’s an example that shows why things are the way they are. Suppose you have a template Foo defined like this:

Along with similar definitions for the member functions:

Now suppose you have some code in file Bar.cpp that uses Foo<int> :

Clearly somebody somewhere is going to have to use the “pattern” for the constructor definition and for the someMethod() definition and instantiate those when T is actually int . But if you had put the definition of the constructor and someMethod() into file Foo.cpp , the compiler would see the template code when it compiled Foo.cpp and it would see Foo<int> when it compiled Bar.cpp , but there would never be a time when it saw both the template code and Foo<int> . So by rule #2 above, it could never generate the code for Foo<int>::someMethod() .

A note to the experts: I have obviously made several simplifications above. This was intentional so please don’t complain too loudly. If you know the difference between a .cpp file and a compilation unit, the difference between a class template and a template class, and the fact that templates really aren’t just glorified macros, then don’t complain: this particular question/answer wasn’t aimed at you to begin with. I simplified things so newbies would “get it,” even if doing so offends some experts.

Reminder: Read the next two FAQs for some solutions to this problem.

How can I avoid linker errors with my template functions?

This answer will be updated due to C++11 extern template . Watch this space for updates in the near future!!

Tell your C++ compiler which instantiations to make while it is compiling your template function’s .cpp file.

As an example, consider the header file foo.h which contains the following template function declaration:

Now suppose file foo.cpp actually defines that template function:

Suppose file main.cpp uses this template function by calling foo<int>() :

If you compile and (try to) link these two .cpp files, most compilers will generate linker errors. There are two solutions for this. The first solution is to physically move the definition of the template function into the .h file, even if it is not an inline function. This solution may (or may not!) cause significant code bloat, meaning your executable size may increase dramatically (or, if your compiler is smart enough, may not; try it and see).

The other solution is to leave the definition of the template function in the .cpp file and simply add the line template void foo<int>(); to that file:

If you can’t modify foo.cpp , simply create a new .cpp file such as foo-impl.cpp as follows:

Notice that foo-impl.cpp #include s a .cpp file, not a .h file. If that’s confusing, click your heels twice, think of Kansas, and repeat after me, “I will do it anyway even though it’s confusing.” You can trust me on this one. But if you don’t trust me or are simply curious, the rationale is given earlier.

How does the C++ keyword export help with template linker errors?

This answer will be updated due to C++11 extern template . Watch this space for updates in the near future!!

The C++ keyword export was originally designed to eliminate the need to include a template definition (either by providing the definition in the header file or by including the implementation file). However, only a few compilers ever supported this capability, such as Comeau C++ and Sun Studio, and the general consensus was that it was not worth the trouble.

Because of that, in the C++11 standard, the export feature has been removed from the language. It remains a reserved word but it no longer has any meaning.

If you are working with a compiler that supports the export keyword, it will probably continue to support the keyword via some sort of compiler option or extension until its users migrate away from it. If you already have code that uses export , you can use a fairly simple discipline to allow your code to easily migrate if/when your compiler stops supporting it entirely. Just define your template header-files like this:

And define your non-inline functions in a source-file like this:

Then compile with -DUSE_EXPORT_KEYWORD , or whatever equivalent compiler option lets you set a preprocessor symbol like USE_COMPILER_KEYWORD , and if/when your compiler removes support for export , just remove that compiler option.

How can I avoid linker errors with my template classes?

This answer will be updated due to C++11 extern template . Watch this space for updates in the near future!!

Tell your C++ compiler which instantiations to make while it is compiling your template class’s .cpp file.

(If you’ve already read the previous FAQ, this answer is completely symmetric with that one, so you can probably skip this answer.)

As an example, consider the header file Foo.h which contains the following template class. Note that method Foo<T>::f() is inline and methods Foo<T>::g() and Foo<T>::h() are not.

Now suppose file Foo.cpp actually defines the non- inline methods Foo<T>::g() and Foo<T>::h() :

Suppose file main.cpp uses this template class by creating a Foo<int> and calling its methods:

If you compile and (try to) link these two .cpp files, most compilers will generate linker errors. There are two solutions for this. The first solution is to physically move the definition of the template functions into the .h file, even if they are not inline functions. This solution may (or may not!) cause significant code bloat, meaning your executable size may increase dramatically (or, if your compiler is smart enough, may not; try it and see).

The other solution is to leave the definition of the template function in the .cpp file and simply add the line template class Foo<int>; to that file:

If you can’t modify Foo.cpp , simply create a new .cpp file such as Foo-impl.cpp as follows:

Notice that Foo-impl.cpp #include s a .cpp file, not a .h file. If that’s confusing, click your heels twice, think of Kansas, and repeat after me, “I will do it anyway even though it’s confusing.” You can trust me on this one. But if you don’t trust me or are simply curious, the rationale is given earlier.

If you are using Comeau C++, you probably want to learn about the export keyword.

Why do I get linker errors when I use template friends?

Ah, the intricacies of template friends. Here’s an example of what people often want to do:

Naturally the template will need to actually be used somewhere:

And of course the various member and friend functions will need to be defined somewhere:

The snag happens when the compiler sees the friend lines way up in the class definition proper. At that moment it does not yet know the friend functions are themselves templates; it assumes they are non-templates like this:

When you call the operator+ or operator<< functions, this assumption causes the compiler to generate a call to the non-template functions, but the linker will give you an “undefined external” error because you never actually defined those non-template functions.

The solution is to convince the compiler while it is examining the class body proper that the operator+ and operator<< functions are themselves templates. There are several ways to do this; one simple approach is pre-declare each template friend function above the definition of template class Foo :

Also you add <> in the friend lines, as shown:

After the compiler sees that magic stuff, it will be better informed about the friend functions. In particular, it will realize that the friend lines are referring to functions that are themselves templates. That eliminates the confusion.

Another approach is to define the friend function within the class body at the same moment you declare it to be a friend . For example:

Why can’t I define constraints for my template parameters?

(Note: This FAQ is a bit dated and needs to be updated for static_assert .)

Well, you can, and it’s quite easy and general.

If there is a type error, it will be in the resolution of the fairly complicated for_each() call. For example, if the element type of the container is an int , then we get some kind of obscure error related to the for_each() call (because we can’t invoke Shape::draw() for an int ).

To catch such errors early, you can write:

The initialization of the spurious variable p will trigger a comprehensible error message from most current compilers. Tricks like this are common in all languages and have to be developed for all novel constructs. In production code, you’d probably write something like:

This makes it clear that you’re making an assertion. The Can_copy template can be defined like this:

Can_copy checks (at compile time) that a T1 can be assigned to a T2 . Can_copy<T,Shape*> checks that T is a Shape* or a pointer to a class publicly derived from Shape or a type with a user-defined conversion to Shape* . Note that the definition is close to minimal:

  • one line to name the constraints to be checked and the types for which to check them
  • one line to list the specific constraints checked (the constraints() function)
  • one line to provide a way to trigger the check (the constructor)

Note also that the definition has the desirable properties that

  • You can express constraints without declaring or copying variables, thus the writer of a constraint doesn’t have to make assumptions about how a type is initialized, whether objects can be copied, destroyed, etc. (unless, of course, those are the properties being tested by the constraint)
  • No code is generated for a constraint using current compilers
  • No macros are needed to define or use constraints

Current compilers give acceptable error messages for a failed constraint, including the word “constraints” (to give the reader a clue), the name of the constraints, and the specific error that caused the failure (e.g. “cannot initialize Shape* by double* ”)

So why is something like Can_copy() – or something even more elegant – not in the language? A way to specify these constraints directly is being worked on as we speak – see Concepts Lite.

Until then, the above the idea is very general. After all, when we write a template we have the full expressive power of C++ available. Consider:

Actually, Derived_from doesn’t check derivation, but conversion, but that’s often a better constraint. Finding good names for constraints can be hard.

How can any human hope to understand these overly verbose template-based error messages?

Compiler error messages have got much better in recent years, and show human-readable typedefs, as well as highlighting where in the source-code the error occurred.

If you’re still using an older compiler, here’s a free tool that transforms error messages into something more understandable. The tool is no longer being developed, but worked with the following compilers: Comeau C++, Intel C++, CodeWarrior C++, GCC, Borland C++, Microsoft Visual C++, and EDG C++.

Here’s an example showing some unfiltered GCC error messages:

Here’s what the filtered error messages look like (note: you can configure the tool so it shows more information; this output was generated with settings to strip things down to a minimum):

Here is the source code to generate the above example:

Why am I getting errors when my template uses a nested type?

Perhaps surprisingly, the following code is not valid C++, even though some compilers accept it:

The reason is that in theory the function template could be called with a type that has a data member or a member function called iterator . When the compiler is parsing the template contains it doesn’t know anything about the types it will be passed by the code that comes later. This means that until the compiler knows what Container is and what members it has, there’s no way to know whether Container::iterator is a type or not. In fact, the rules of C++ say that until told otherwise, the compiler must assume Container::iterator is not a type. The solution is to give the compiler a hint via the typename keyword:

This tells the compiler that when the function template is used later its argument will always be something for which Container::iterator is a type. (And if you actually try to call it with a type where iterator is a data member or something else, you will get an error).

Why am I getting errors when my template-derived-class uses a nested type it inherits from its template-base-class?

Perhaps surprisingly, the following code is not valid C++, even though some compilers accept it:

This might hurt your head; better if you sit down.

Within D<T>::g() , name Xyz and Pqr do not depend on template parameter T , so they are known as a nondependent names. On the other hand, B<T> is dependent on template parameter T so B<T> is called a dependent name.

Here’s the rule: the compiler does not look in dependent base classes (like B<T> ) when looking up nondependent names (like Xyz or Pqr ). As a result, the compiler does not know they even exist let alone are types.

At this point, programmers sometimes prefix them with B<T>:: , such as:

Unfortunately this doesn’t work either because those names (are you ready? are you sitting down?) are not necessarily types. “Huh. ” you say. “Not types. ” you exclaim. “That’s crazy; any fool can SEE they are types; just look. ” you protest. Sorry, the fact is that they might not be types. The reason is that there can be a specialization of B<T> , say B<Foo> , where B<Foo>::Xyz is a data member, for example. Because of this potential specialization, the compiler cannot assume that B<T>::Xyz is a type until it knows T . The solution is to give the compiler a hint via the typename keyword:

Why am I getting errors when my template-derived-class uses a member it inherits from its template-base-class?

Perhaps surprisingly, the following code is not valid C++, even though some compilers accept it:

This might hurt your head; better if you sit down.

Within D<T>::g() , the name f does not depend on template parameter T , so f is known as a nondependent name. On the other hand, B<T> is dependent on template parameter T so B<T> is called a dependent name.

Here’s the rule: the compiler does not look in dependent base classes (like B<T> ) when looking up nondependent names (like f ).

This doesn’t mean that inheritance doesn’t work. Class D<int> is still derived from class B<int> , the compiler still lets you implicitly do the is-a conversions (e.g., D<int>* to B<int>* ), dynamic binding still works when virtual functions are invoked, etc. But there is an issue about how names are looked up.

  • Change the call from f() to this->f() . Since this is always implicitly dependent in a template, this->f is dependent and the lookup is therefore deferred until the template is actually instantiated, at which point all base classes are considered.
  • Insert using B<T>::f; just prior to calling f() .
  • Change the call from f() to B<T>::f() . Note however that this might not give you what you want if f() is virtual, since it inhibits the virtual dispatch mechanism.

Can the previous problem hurt me silently? Is it possible that the compiler will silently generate the wrong code?

Since non-dependent types and non-dependent members are not found in the dependent template base-classes, the compiler will search the enclosing scope, such as the enclosing namespace. This can cause it to silently(!) do the wrong thing.

The use of Xyz and f within D<T>::g() will silently(!) resolve to the global entities rather than those inherited from class B<T> .

You have been warned.

How can I create a container-template that allows my users to supply the type of the underlying container that actually stores the values?

First, let’s clarify the question: the goal is to create a template Foo<> , but having the template parameter-list include some particular type of std::vector<T> or std::list<T> or some other (possibly non-standard) container to actually store the values.

Here’s one way to do that:

If you want to allow your users to provide you with an underlying container that does not necessarily have a value_type typedef (such as some container from a third party), you could provide the value-type explicitly:

However you cannot (yet) provide an unspecified template as a template parameter, such as this:

Follow-up to previous: can I pass in the underlying structure and the element-type separately?

Yes, with a “proxy” trick.

Here’s the problem: std::vector template can have, does have, more than one argument. You’re required to make them match in the number, order, and nature — type/non-type, etc.

It is possible, however, to “cheat” your way out of specifying all those arguments and use the defaults. It’s called the “proxy template” technique:

You can also create a proxy if you actually have a template that takes only one argument:

The “template typedefs” proposal will allow redefining templates just like we do with types, which will make the proxy-trick unnecessary. Until then, use something like the above.

Related: all those proxies must negatively reflect on the speed of my program. Don’t they?

They might require an extra microsecond to compile, but once all the types are resolved by the compiler, the resulting code is just as fast as if you used them directly without any proxy templates whatsoever.

Not only that, there are some techniques (template meta programming or TMP) that can, under the right circumstances, improve the efficiency of the generated code. The basic notion is to get the compiler to do more work at compile-time so less work has to be done at runtime.

Лекция 8. Шаблоны

Шаблоны (templates) — это механизм, позволяющий обобщать функции и классы для работы с различными типами данных.

Шаблоны обеспечивают непосредственную поддержку обобщенного программирования (т.е. c использованием типов в качестве параметров)

Шаблон зависит только от тех свойств параметра-типа, которые он явно использует

Существуют шаблоны функций и классов

Инстанцирование

Процесс порождения функции или класса из шаблона называется инстанцированием

Процесс генерации объявления класса по шаблону класса и аргументу шаблона

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

Генерация версий шаблона — задача компилятора

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

Для создания шаблона используется ключевое слово template. Также указывается пока неопределенный тип T.

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

Для получения функции проведем инстанцирование

Еще один пример: функция сортировки

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

Примеры использования шаблонов

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

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

Воспользоваться шаблоном класса можно так

Описание конструктора и деструктора шаблонного класса

Описание методов push и pop

Описание методов определения размера стека

Примеры инстанцирования

Рассмотрим примеры использования шаблона стека

1.3 Параметры шаблонов

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

У шаблонов могут быть параметры различных типов

Инстанцирование выполняется с указанием значения параметра

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

Если в шаблоне класса или функции необходимо использовать один и тот же шаблон, но с разными параметрами, то используются параметры-шаблоны. Например:

Перегрузка шаблонов
Специализация шаблонов
Явное инстанцирование

Явное инстанцирование используется

  1. Если инстанцирование шаблонов отнимает слишком много времени
  2. Если порядок компиляции должен быть абсолютно предсказуем

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

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

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

Для строк создаётся специализация.

2.1 Класс Bag

Шаблон класса Bag

В следующем примере приводится шаблон класса Bag, который является

динамическим контейнером элементов и его специализация,

позволяющая задавать элементы не по значению, а по указателю.

2.2 Пример с наследованием

Мы можем использовать специализацию при наследовании.

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

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

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

Описываем шаблон класса-наследника, в который помещается метод GetBaseClass

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *