Шаблоны

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

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

  • Чтобы специализировать шаблон класса, следует объявить класс, предварив его конструкцией template<>, и указать типы, для которых специализируется шаблон класса. Типы используются в качестве аргументов шаблона и задаются непосредственно после имени класса.

Например, я хочу сложить два значения. В случае с числом, сложением является сумма чисел, а в случае со строкой под сложением можно понимать объединение строк. Вначале написания кода не должно возникнуть проблем

Так вот, этот пример прекрасно работает и складывает 2 значения. Но если его слегка дополнить, изменить main

Для Си строк нет перегруженного оператора сложения, так как нельзя написать непосредственно std::cout<<"Hello" + "Bye", то такая конструкция и не сработает. Чтобы решить такую незадачку, нужно помочь компилятору, научить его тому, как он должен себя вести. Так как тема, описываемая мною сейчас — это тема "Шаблоны в С++", то и будем придерживаться шаблонов. Так вот, один из вариантов помощи компилятору, это специализировать метод класса или сам класс. Предпочтительнее специализировать класс, чем отдельный его метод. Но начнем с того, что попроще.

  • Чтобы специализировать шаблон класса, следует объявить класс, предварив его конструкцией template<>, и указать типы, для которых специализируется шаблон класса.
  • Типы используются в качестве аргументов шаблона и задаются непосредственно после имени класса.

Похожее правило и для метода класса. Пример специализации отдельного метода.

Этот пример в самую первую очередь должен заставлять задавать вопрос: "Где очистка?" А вот нету очистки памяти. А чтобы почистить память в данном случае, нужно возиться и придумывать себе что-то. Это одна из причин того, что при специализации шаблона класса необходимо специализировать все его функции-члены.

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

  • При специализации шаблона класса необходимо специализировать все его функции-члены.

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

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

Для тех кто не знаком с конструктором по умолчанию, хоть и пояснял, но еще раз поясняю
MyClass::MyClass(int a, int b):x(a),y(b){};
означает, что в приватное поле класса MyClass в переменную x будет записано значение, пришедшее как а и в y соответственно b.

Остается надеяться, что эта тема оказалась полезной.

Variadic templates. Шаблоны с переменным числом аргументов. С++11

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

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

Это может быть полезным когда число параметров для передачи в функцию заранее неизвестно. Напишем простой код, который только выводит на экран переменные, отданные в шаблонную функцию.
Своеобразный прием "HELLO WORLD" для появившейся возможности.

Для начала опишем простую шаблонную функцию

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

Чтобы это рассказать, нужно использовать такой синтаксис

Здесь используется мета-операция троеточие. В этом синтаксисе можно подумать, что args — это переменная типа Args, когда Args — это тип, распознанный шаблоном. Это близко к действительности, но это не так. Args и args — это пакеты параметров.

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

Показанный выше синтаксис шаблонной функции будет соответствовать любому из вызовов функций

Пример компилируется и выполняется успешно в С++11 (на экран ничего не выводим)

  • Только параметры шаблонов и параметры функций могут быть пакетами параметров

С созданием шаблонов с переменным числом аргументов разобрались. Сами-то шаблоны создали, они работают. Следующий по логике вопрос: "Как работать с такими аргументами"
Привычными приемами (типа взятия индекса c помощью []) тут работать не выйдет. Никаких средств индексирования не существует. Для того, чтобы "копнуть в пакет" и вытащить оттуда тип, существует возможность распаковки пакета. Чтобы распаковать пакет, нужно справа от пакета (пакета параметра функции) поставить троеточие. А чтобы обработать такой распакованный пакет, можно использовать функцию. Кроме использования функций существуют и другие способы. Один из них я продемонстрирую сейчас, но все другие способы здесь оговариваться не будут. Для начала надо разобраться с наиболее коротким и наиболее употребляемым вариантом. Но он чуть позднее.

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

Вот такой вот пример.

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

Такой код компилируется, запускается и аварийно завершается. Здесь рекурсия никогда не заканчивается, а бесконечная рекурсия в принципе невозможна. При каждом новом вызове функции в функцию отдается один и тот же пакет. Для каждого нового вызова новая порожденная функция забирает весь пакет полностью. Заканчивается место в стеке и "приплыли". Но смысл показанного в том, чтобы показать, что такое написание в С++11 допускается. Тут нужно рассказать компилятору когда заканчивать рекурсию. Чтобы научить компилятор заканчивать рекурсию, используют не сложный трюк. Нужно при каждом новом вызове функции обрабатывать первый аргумент, а оставшуюся часть без первого аргумента упаковывать в пакет. Делается это не сложно. Нужно объяснить в шаблоне, что первый аргумент — это обычный тип данных, а остальная часть пакет. Тогда на каждом новом витке рекурсии, из пакета будет вытащен первый аргумент, у него будет распознан тип, а остальной пакет будет пакетом из оставшихся аргументов. Синтаксически выглядит это так:

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

Теперь всё работает. В зависимости от ситуации может может потребоваться перегрузка функции с одним аргументом, а не функции без аргументов, тогда и перегружать надо немного по другому

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

И в завершении темы, привожу два примера. Они демонстрируют возможную зависимость от ситуации. А заодно закрепляют эти важные познания С++

1. Выводим на экран переменные. Заранее неизвестное количество. Заранее неизвестны типы.

2. Считаем сумму

C++. Шаблоны. Шаблоны как методы классов.

Всегда необходимо полностью указывать любое имя, объявленное в базовом классе, который каким-либо образом зависит от параметра шаблона. Для этого можно использовать конструкции this-> или Base::.
Чтобы гарантированно исключить какую бы то ни было неопределенность, можно использовать полное имя при любом обращении к членам классов (в шаблонах).
===========================================
Упражнение обязательное для понимания этой темы.
Создание класса. В private поле этого класса одну переменную. В public поле этого класса — конструктор, геттер. Нужно перегрузить оператор =. Т.е. внутри класса всего 4 поля.

Вот такой вод вид у класса. Во внутренние поля ничего добавлять не надо. Только применить этот псевдовариант в коде и заставить его заработать. Методы описывать можно как внутри класса, так и вне. Я, как автор сайта, начинал именно с такого вот момента. Если Вы прекрасно ориентируетесь в перегрузке операторов, то, не обязательно выполнять. Но если нет, то без малейшего понимания перегрузки операторов, а в данном случае оператора присваивания, вся это тема будет хуже вообще незнакомого языка.
В результате правильной работы должно получится приблизительно так:

==========================================

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

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

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

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

Вот рабочий код с таким присваиванием

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

Дело в том, что используемый по умолчанию оператор присвоения требует одинаковости типов с обеих своих сторон, а здесь слева double (тип Num3), а справа int (тип Num1). Если бы это были обычные встроенные типы, то не было бы больших проблем, а так как у нас классы, то вот возникают. Не умеет компилятор так глубоко копать, будьте любезны и научите его.

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

Проверка на присваивание самому себе нужна, чтобы избежать ненужного копирования в больших объектах. (стеки, очереди..). В этом примере такое копирование не столь затратно. Следует быть особо внимательным к указанием типов. И не стоит забывать, что это НЕ идеальный, но учебный пример, который можно и нужно допилить. Это всего-лишь ознакомление, начало знакомства.

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

22-23 строчки последнего листинга читаются как: "Внутри шаблона с параметром T определяется внутренний шаблон с параметром Т2 "

Еще стоит отметить такой момент, что

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

C++. Шаблоны. Параметры шаблонов как значения

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

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

Чтобы это показать в достаточно простом варианте, будет использован класс. Вот на такой основе.

Вот более полный класс.

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

Теперь, если MAXSIZE не указывать в параметрах при вызове, то MAXSIZE будет принимать значение 100.

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

addValue — обращение к шаблонной функции, в Т отдаем тип int, а в VAL отдаем 5.

С++. Аргументы по умолчанию у шаблонов класса.

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

Вот параметры по умолчанию в шаблонных классах действуют точно по такому же принципу. Сейчас будет немного тяжеловато. Для примера выбраны контейнеры STL (в старых компиляторах STL нет), но более менее наглядно можно показать, используя STL. Я не хочу описывать 2 класса-велосипеда. Поэтому выбор для описания именно такой какой он есть.

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

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

Такой вот код . В начале шаблонному классу был скормлен только один параметр. Этот параметр пришел в первый аргумент, в T. Второй параметр мы не отдавали, но в шаблонном классе второй аргумент описан как аргумент по умолчанию, поэтому, если мы не отдаем в него параметров, он автоматически присваивает себе то значение, которое ему указано. Так, в этом примере, аргументу по умолчанию было указано, чтобы он принимал тип vector (Вектор, хранящий целые числа). Так и вышло, что в объекте класса MyClass1 образовался вектор для целочисленных значений и в него мы стали записывать данные.
Во втором же случае, вторым параметром, было указано, что второй параметр — это deque, хранящий данные тип у которых string, а следовательно во второй аргумент шаблонного класса пришел именно этот deque, он переписал тот тип, который был указан по умолчанию. Своего рода захватчик. Поэтому во втором случае в объекте класса образовалась очередь deque, хранящая данные, тип у которых string.

Попробуйте создать еще один объект

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

В STL и у std::vector и у std::deque есть одноименный метод push_back(). Для тех, кто незнаком с STL и это не очень понятно, можете считать, что есть 2 класса. В этих обоих классах есть метод с одинаковым названием. Выполняет этот метод для одного класса одно, а для другого, соответственно другое. Как двигатели под капотом разных автомобилей. Общее у них только название. И вот, благодаря общему названию, и указанию аргумента по умолчанию, получается использовать либо метод первого класса, либо же метод второго класса. Это все зависит от того, указан ли параметр при вызове, который должен переписать аргумент шаблона, указанный по умолчанию или же не указан.

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

Шаблоны функций. Аргументы по умолчанию. С++11

Это небольшая статья показывает маленький фокус с обработкой матриц. До С++11 обработка двумерных и более мерных матриц внутри функций требовала того, чтобы программисты С++ явно указывали саму матрицу и ее размеры. Либо использования двумерного массива как одномерного, либо другого извращенного варианта. Язык С++ развивается и дополняется различными возможностями. С одной стороны это плохо, с другой удобства — это удобства. Поэтому простой прием обработки двумерного статически создаваемого массива внутри функции.

У нас с Вами будет очень простая задача. Показать двумерный массив на экране при помощи функции.

Вот наш массив

int m[3][3] {{ 1, 2, 3}, {4, 5, 6}, { 8, 9, 10}};

Прошу заметить, что это не массив **, а массив [][], т.е. статически создаваемый. Вот этот массив мы и будем выводить на экран. Начнем с самого простого способа, используя возможности С++11.

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

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

Но что делать, если массив не такой "ровный", а прямоугольный, где число строк и колонок разнится. То, что массив способен распознать часть своего размера не обозначает, что это сможем сделать мы, да и этот показанный прием даст сбой. Вот пример такого сбоя.

И на экране нет массива! Сбой, прием не работает. Это не сбой, это наша (в текущей ситуации моя) ошибка. Достаточно посмотреть, что получается в size1 и в size2 с помощью cout. (Думаю, это Вы осилите и без моей помощи). Получим массив [0][4]. Когда функция foo инстанирует шаблон, то этот шаблон создаст массив m[0][4], а это совсем не наш [3][4]. Вы же можете создать, например, нулевой массив int Arr[0][0]; А заполнить? Но то только пол беды. А если бы и не ноль строк, узнать число элементов сам массив может и может, а мы вот по идее не можем. Внутри функции получается указатель, а размер указателя — это не размер массива. В первом успешном варианте нас спасло то, что мы знали, что число строк и колонок одинаковое, а сейчас-то нам число строк неизвестно совсем. Но, решение есть и оно, как оказывается, очень даже не сложное. Я для наглядности size1 и size2 поменяю на Row и Col соответственно, это не должно Вас смутить.

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

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

С++. Параметры шаблонов функций.

Эта тема — продолжение знакомства с шаблонами функций. В предыдущей теме Шаблоны функций в С++ для начинающих Первое знакомство был показан пример вывода наибольшего числа из двух с помощью шаблонной функции. Проблемным моментом в той функции был тип возвращаемого значения. Это просто логическое продолжение того материала.

Для начала листинг кода такой вот несложной функции

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

Думать о том, в каком порядке передавать аргументе — дело не благодарное. А в ином случае вот такие вот фокусы происходят.
Другая проблема не столь очевидная в том, что Т2 может неявно приводиться к Т1. Если немного подумать, можно легко понять, что функция возвращает тип Т1. А тип Т2 может не совпадать с типом Т1. Что если переменная типа Т2 больше чем переменная типа Т1 ? Возвращать надо Т2, но так типы не совпадают, происходит приведение типа из Т2 в Т1. Для этого приведения внутри функции создается внутренняя локальная переменная. Живет она внутри функции и умирает вместе с завершением работы функции. В свою очередь из-за этого нельзя вернуть результат по ссылке.

Такие вот недостатки у такого очевидного способа.
Один из вариантов обхода такого неудобства — это задавать дополнительный параметр вывода в шаблон.

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

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

Теперь все заработало. В угловых скобках мы явно указали тип данных, который ожидаем от функции, этот тип данных пришел в RT. Остальные типы пришли в T1 и в T2. Они туда ушли молча. Ушли в том порядке, в котором мы отдаем их вовнутрь функции. Так в первом случае (первой строчке) в T1 ушел тип из переменной N2 (int), в T2 тип N1 (double), а во втором случае T1 из N1(double),T2 из N2(int).

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

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

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

Поиск

 
     

Яндекс.Метрика

НАГРАДИ АВТОРА САЙТА
WEBMONEY
R375024497470
U251140483387
Z301246203264
E149319127674

Демотиватор рекурсии

Выражаю свою признательность

  • Максиму очень признателен за указание на мои ошибки и неточности.
  • Sergio ===> за оказание помощи в исправлении моих ошибок
  • Gen ===> за правильное стремление помочь другим новичкам и выявления моих ошибок