В С++ существует возможность создания {функции с переменным числом параметров}. В С++11 способ создания такой функции возымел новый способ. Стало возможным задать {шаблон с переменным количеством параметров}. Шаблоны функций — это такая замечательная вещь, которая умеет подставлять типы под переменные внутри функции.
Шаблоны с переменным числом параметров предоставляют средство создания шаблонных функций и шаблонных классов, которые принимают переменное количество аргументов.
Для тех, кто не в теме моего авторского предпочтения, небольшое пояснение: я называю аргументами те значения, которые мы пишем в момент вызова, а параметрами те значения, которые принимаются функцией или шаблоном. Иногда их называют наоборот.
Переменное число параметров шаблона может быть полезно, когда число аргументов для передачи в функцию или аргументов для передачи в класс заранее неизвестно. Напишем простой код, программа из которого только выводит на экран переменные, отданные в шаблонную функцию. Своеобразный приём "HELLO WORLD" для появившейся возможности.
Для начала опишем простую шаблонную функцию:
C++
1
2
3
4
5
//Листинг #1
template<typenameT>
voidfoo(Tvalue){
}
Начало знакомства с переменным числом параметров шаблонов предполагает, что нужно хорошо понимать, что такое написано в листинге #1. В противном случае имеет смысл перейти к началу знакомства с шаблонами. В функцию foo придёт какое-то данное определённого типа (придёт входящее значение-аргумент), а функция сама распознает тип входящего значения-аргумента для использования. Это вкратце. Но эта функция для одного аргумента. Если аргументов от одного до ста, то уже не совсем приятно перегружать более сотни функций, в каждой из которых описывать приблизительно одинаковый код. Это должно быть интуитивно понятно, что напряжно сделать и сложно потом исправлять. Ведь исправление одной функции зачастую повлечёт исправление каждой из функций.
Чтобы избежать подобной участи (и по другим причинам), в С++11 добавлена возможность рассказать шаблонным функциям и классам, что число аргументов и типы этих аргументов заранее неизвестны. Для этого нужно использовать такой синтаксис:
C++
1
2
3
4
5
//Листинг #2
template<typename...Args>//Объясняемся перед шаблоном, что в функции будет неизвестное число аргументов
voidshow_listl(Args...args)//Объясняемся перед функцией, что число аргументов неизвестно
{
}
В листинге #2 используется мета-операция троеточие. Разбирая подобный синтаксис, можно подумать, что args — это переменная типа Args, когда Args — это тип, распознанный шаблоном. Это близко к действительности, но это не так. Args и args — это пакеты параметров.
Разъясняю своими словами (могу ненароком наврать, но попробую):
Начало объяснения моими словами.
В шаблоне получается один целостный тип из множества типов, отданных в функцию. Сам по себе он один тип. Просто состоит из составных частей, где каждая часть является отданным в функцию типом. Вот так и получается, что тип для шаблона будет один. Этот один тип и назван пакетом (упаковкой, паком). Как угодно можно назвать. Просто внутри себя он полностью наполнен типами. А внутри функции объявлена переменная, тип которой и есть список типов (пакет). Так как с троеточия начинать объявление типов нельзя, то троеточие внутри функции указано после типа (справа от Args), а само по себе троеточие обозначает, что это не отдельная переменная одного конкретного типа, а принадлежность этой переменной к одному из типов внутри пакета.
Конец объяснения моими словами.
Показанный в листинге #2 синтаксис шаблонной функции будет соответствовать любому из вызовов, показываемых в листинге #3:
C++
1
2
3
4
5
//Листинг #3
foo();
foo(99);
foo(88.5,"cat");
foo(2,4,6,8,"who do we",string("appreciate"));
Полный пример показывается в листинге #4, он компилируется (С++11 и старше), и программа срабатывает. На экран ничего не выводим.
C++
1
2
3
4
5
6
7
8
9
10
11
#Листинг #4
template<typename...Args>// Args — это пакет параметров шаблона
voidfoo(Args...args)// args — это пакет параметров функции
{
}
intmain()
{
foo(89.8,"HELLO",5);//
}
Только параметры шаблонов и параметры функций могут быть пакетами параметров.
С созданием {шаблонов с переменным числом параметров} разобрались. Сами-то шаблоны создали, они работают. Логичный вопрос, который может возникнуть теперь: "Как работать с такими параметрами?". Привычными приёмами (типа взятия индекса c помощью []) тут работать не выйдет. Никаких средств индексирования на подобную цель не существует. Для того, чтобы "копнуть в пакет" и вытащить оттуда тип, существует возможность распаковки пакета. Чтобы распаковать пакет, нужно справа от пакета (пакета параметра функции) поставить троеточие. А чтобы обработать такой распакованный пакет, можно использовать функцию. Кроме использования функций существуют и другие способы. Один из них я продемонстрирую сейчас, но все другие способы здесь оговариваться не будут. Для начала надо разобраться с наиболее коротким и наиболее употребляемым вариантом. Но он чуть позднее.
Один из способов для демонстрации возможности использования распакованного пакета будет показан в листинге #5. Я надеюсь, что листинг хорошо покажет, что понимается под пакетом и чем является распакованный пакет.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//Листинг #5
#include <iostream>
#include <string>
usingnamespacestd;
structMyStruct{//Структура наших параметров, отдаваемых в функцию.
doubled;
strings;
inti;
};
template<typename...T2>// Args — это пакет параметров шаблона
voidfoo(T2...t)// args — это пакет параметров функции
{
MyStruct obj={t...};//Инициализируем объект структуры нашим пакетом
cout<<obj.d<<' '<<obj.i<<' '<<obj.s<<'\n';//Выводим данные пакета на экран
}
intmain()
{
stringS("HELLO");
foo(89.8,S,5);//Типы в порядке следования будут соответствовать типам внутри структуры
cin.get();
}
Распакованный пакет можно передавать вовнутрь функций. А это обозначает, что можно использовать рекурсивные вызовы с использованием распаковки. Выглядит это так:
C++
1
2
3
4
5
6
7
8
9
10
11
//Листинг #6
template<typename...Args>// Args — это пакет параметров шаблона
voidfoo(Args...args)// args — это пакет параметров функции
{
foo(args...);// <--- Рекурсивные вызовы. В функцию отдаем распакованный пакет
}
intmain()
{
foo(89.8,"HELLO",5);
}
Код, показанный в листинге #6 компилируется, запускается и аварийно завершается. Дело в том, что рекурсия никогда не заканчивается, а бесконечная рекурсия в принципе в нашем программировании невозможна. При каждом новом вызове функции в функцию отдаётся один и тот же пакет. Для каждого нового вызова новая порождённая функция забирает весь пакет полностью. Заканчивается место в стеке и "приплыли". Но идея демонстрируемого в том, чтобы показать, что такое написание в С++11 допускается. Нужно рассказать компилятору, когда заканчивать рекурсию. Чтобы научить компилятор заканчивать рекурсию, используют несложный трюк: нужно при каждом новом вызове функции обрабатывать первый аргумент, а {оставшуюся часть без первого аргумента} упаковывать в пакет. Делается это следующим образом: нужно объяснить в шаблоне, что первый аргумент — это обычный тип данных, а остальная часть — пакет. Тогда на каждом новом витке рекурсии из пакета будет вытащен первый аргумент, у него будет распознан тип, а остальной пакет будет пакетом из оставшихся аргументов. Синтаксически выглядит это так:
C++
1
2
3
4
5
6
7
8
9
10
11
//Листинг #7
template<typenameT,typename...Args>// Args — это пакет параметров шаблона
voidfoo(Tvalue,Args...arg)// args — это пакет параметров функции
{
foo(arg...);// <--- Рекурсивные вызовы
}
intmain()
{
foo(89.8,"HELLO",5);
}
Код из листинга #7 не может быть скомпилирован. Причина его некомпиляции проста. В функции, на одном из витков рекурсии, не остается ни одного из параметров. А есть ли у нас функции без параметров? В том-то и дело, что таких функций у нас нет, поэтому компилятор в тупике. Он не понимает, что надо вызывать. Нужно ему помочь описанием такой функции.
template<typenameT,typename...Args>// Args — это пакет параметров шаблона
voidfoo(Tvalue,Args...arg)// args — это пакет параметров функции
{
foo(arg...);// <--- Рекурсивные вызовы
}
intmain()
{
foo(89.8,"HELLO",5);
}
Теперь всё работает. В зависимости от ситуации может потребоваться перегрузка функции с одним параметром, а не функции без параметров, тогда и перегружать надо немного по-другому:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Листинг #9
template<typenameT>
voidfoo(Tvalue){}//Функция будет вызвана, когда идет обращение к функции с одним принимаемым параметром.
template<typenameT,typename...Args>// Args — это пакет параметров шаблона
voidfoo(Tvalue,Args...arg)// args — это пакет параметров функции
{
foo(arg...);// <--- Рекурсивные вызовы
}
intmain()
{
foo(89.8,"HELLO",5);
}
Хочу заметить, что в функцию могут передаваться большие объекты. Тогда обычно нужно описывать функции таким способом, чтобы параметры принимались по константной ссылке или просто по ссылке.
И в завершение темы привожу два примера. Они демонстрируют возможную зависимость от ситуации. А заодно закрепляют эти важные познания С++:
Спасибо за статью, удачные простые примеры. Многое стало понятней.
Заметил, что в последнем примере нужно возвращать value, а не ноль. Вот правильный вариант:
автор источники надо указывать ваша статья полностью взята из книги Стивена Прата «Язык С++11» 2012 года издания.
Да, это написано не без помощи книги Прата.
Только такое заявление может сделать тот, кто невнимательно читает. Отличие есть. Не полностью.
foo(2, 4, 6, 8, «who do we»,string(«appreciate));
Не хватает ковычки и пространства имён (если имеется в виду std::string)
Пример с выводом на экран и посчётом суммы «съедает» последнее значение из пакета args
Так как оно передаётся в функцию с одним аргументом.
В первом примере: функция с одним аргументом выводит перевод нв новую строку (более ожидаемо, что она выведет последний аргумент)
Во втором примере: функция с одним аргументом возвращает ноль, а более ожидаемо, что она доприбавляет последний элемент к общей сумме
Не знаю, баг это или осознанное решение. Но имхо получилось неочевидно
Это не баг. Я ошибся при написании.
Смотрите сейчас.
оба примера из конца статьи странно компилируются в этом онлайн компиляторе https://www.onlinegdb.com/ и не хотят завершаться
1. Уберите весь русский текст.
2. После Run Нажмите клавишу Enter, либо уберите std::cin.get()
3. Помогло?