функции с переменным количеством параметров
С++ для начинающих Функции с переменным числом параметров
Для того, чтобы получить доступ ко всем параметрам, принимаемых функцией, нужно знать имя и тип хотя бы одного параметра.
Вот такой код не вызывает возражений, а многоточие после первого параметра внутри параметров функции обозначает, что в функцию можно передать для обработки более одного параметра.
Должен возникнуть вопрос как же получить доступ к каждому из этих параметров. Для того, чтоб хорошо понять то что будет написано, требуется знание указателей. Чтобы иметь доступ к каждому из параметров, нужно знать адрес первого параметра, а чтобы взять этот адрес, как раз и требуется указатель
=========
Что можно увидеть? Первым делом внутри функции происходят взятие адреса первого параметра, при этом тип указателя должен совпадать с типом этого параметра. Дальше идет простой перебор всех параметров с помощью цикла.
Таким же образом можно привести классический пример суммирования элементов
while (* P ) //Пока встречаются параметры
<
sum = sum +(* P ) ” “ ; //Прибавляем к сумме то что взяли по адресу P
P ++; //Адресная арифметика. Смена текущего адреса на следующий
>
cout sum endl ; //Вывод результата на экран
>
Вообще весь этот механизм удобно использовать тогда, когда требуется обработка однотипных элементов и чем-то всё напоминает обычный массив элементов. Учитывая, что указатель должен быть того же типа, что и элемент, расположенный по адресу на который он ссылается, можно сказать, что переменное число параметров может быть корректно использовано как раз тогда, когда все параметры передаваемые в такую функцию принадлежат одному и тому же типу (имеются ввиду те параметры, которые идут туда где расположено многоточие).
Но вообще в функцию можно передавать произвольное число параметров с переменным типом, я не говорил, что этого сделать нельзя, можете перечитать всё снова, чтобы в этом убедиться. При определении функций с переменным количеством параметров, рекомендуется использовать специальный набор макроопределений, которые становятся доступными при включении в программу заголовочного файла stdarg.h
Эти макроопределения обеспечивают простой и стандартный (независящий от реализации) способ доступа к спискам параметров переменной длины. Я не буду расписывать, так как сам плохо все это представляю, но приведу пример, содранный с учебника Марченко А.Л. Бархатный путь (2005). Там более менее расписано что к чему и нарушать его авторство я не хочу. Но кое что оттуда вынесу
Функции с переменным числом параметров
Функции с переменным числом параметров
К ак уже обсуждалось ранее, по умолчанию параметры передаются функции через стек. Поэтому, технически, нет ограничения на количество передаваемых параметров – “запихать” можно сколько угодно. Проблема в том, как потом функция будет разбирать переданные параметры. Функции с переменным числом параметров объявляются как обычные функции, но вместо недостающих аргументов ставится многоточие. Пусть мы хотим сделать функцию, которая складывает переданные ей числа, чисел может быть произвольное количество. Необходимо каким-то образом передать функции число параметров. Во-первых, можно явно передать число параметров обязательным аргументом. Во-вторых, последний аргумент может иметь некоторое «терминальное» значение, наткнувшись на которое функция закончит выполнение.
Общий принцип работы следующий: внутри функции берём указатель на аргумент, далее двигаемся к следующему аргументу, увеличивая значение указателя.
OLD SCHOOL
Д елаем всё вручную. Функция, которая складывает переданные ей аргументы
Первый параметр – число аргументов. Это обязательный параметр. Второй аргумент – это первое переданное число, это тоже обязательный параметр. Получаем указатель на первое число
Далее считываем все числа и складываем их. В этой функции мы также при сложении проверяем на переполнение типа unsigned.
Можно сделать первый аргумент необязательным и «перешагнуть» аргумент unsigned char num, но тогда возникнет большая проблема: аргументы располагаются друг за другом, но не факт, что непрерывно. Например, в нашем случае первый аргумент будет сдвинут не на один байт, а на 4 относительно num. Это сделано для повышения производительности. На другой платформе или с другим компилятором, или с другими настройками компилятора могут быть другие результаты.
Поэтому лучше число параметров, если это аргумент, сделать типом int или unsigned int.
Можно сделать по-другому: в качестве «терминального» элемента передавать ноль и считать, что если мы встретили ноль, то больше аргументов нет. Пример
Но теперь уже передавать нули в качестве аргументов нельзя. Здесь также есть один обязательный аргумент – первое переданное число. Если его не передавать, то мы не сможем найти адрес, по которому размещаются переменные в стеке. Некоторые компиляторы (Borland Turbo C) позволяют получить указатель на …, но такое поведение не является стандартным и его нужно избегать.
VA_ARG
М ожно воспользоваться макросом va_arg библиотеки stdarg.h. Он делает практически то же самое, что и мы: получает указатель на первый аргумент а затем двигается по стеку. Пример, та же функция, только с va_arg
Первый аргумент – число параметров – также лучше делать типа int, иначе получим проблему со сдвигом, кратным 4.
Название | Описание |
---|---|
va_list | Тип, который используется для извлечения дополнительных параметров функции с переменным числом параметров |
void va_start(va_list ap, paramN) | Макрос инициализирует ap для извлечения дополнительных аргументов, которые идут после переменной paramN. Параметр не должен быть объявлена как register, не может иметь типа массива или указателя на функцию. |
void va_end(va_list ap) | Макрос необходим для нормального завершения работы функции, работает в паре с макросом va_start. |
void va_copy(va_list dest, va_list src) | Макрос копирует src в dest. Поддерживается начиная со стандарта C++11 |
Неправильное использование
Если передано больше аргументов, то функция выведет только те, которые ожидала встретить
Так как очистку стека производит вызывающая функция, то стек не будет повреждён. Получается, что если изменить схему вызова и сделать так, чтобы вызываемый объект сам чистил стек после себя, то в случае неправильного количества аргументов стек будет повреждён. То есть, буде функция объявлена как __stdcall, в целях безопасности она не может иметь переменного числа аргументов.
Однако, если добавить спецификатор __stdcall к нашей функции summ она будет компилироваться. Это связано с тем, что компилятор автоматически заменит __stdcall на __cdecl.
Программа завершится с ошибкой вроде The value of ESP was not properly saved across a function call.
Функции со списками аргументов переменных (C++)
Функции, в объявлениях которых в качестве последнего члена указано многоточие (. ), могут принимать переменное число аргументов. В таких случаях C++ обеспечивает проверку типа только для явно объявленных аргументов. Переменные списки аргументов можно использовать, если функция должна быть настолько универсальной, что могут изменяться даже количество и типы аргументов. Семейство функций — это пример функций, использующих списки аргументов переменных. printf список-объявление аргументов
Функции с переменными аргументами
Чтобы получить доступ к аргументам после их объявления, используйте макросы, содержащиеся в стандартном файле include, как описано ниже.
Блок, относящийся только к системам Microsoft
Завершение блока, относящегося только к системам Майкрософт
В объявлении функции, которая принимает переменное число аргументов, требуется по крайней мере один аргумент-местозаполнитель, даже если он не используется. Если этот аргумент-местозаполнитель не указан, доступ к остальным аргументам невозможен.
Функции, которым необходимы списки с переменным количеством аргументов, объявляются с многоточием (. ) в списках аргументов. Используйте типы и макросы, описанные в включаемом файле, для доступа к аргументам, передаваемым из списка переменных. Дополнительные сведения об этих макросах см. в разделе va_arg, va_copy, va_end va_start. в документации по библиотеке времени выполнения C.
В следующем примере показано, как макросы работают вместе с типом (объявленным в ):
Приведенный выше пример иллюстрирует следующие важные правила:
Для завершения обработки списка с переменным количеством аргументов необходимо вызвать макрос. va_end
Шаблоны с переменным числом параметров
Кувшинов Д.Р.
Введение
Начиная с версии C++11 язык C++ предоставляет возможность определять шаблоны, принимающие переменное число параметров.
Например, можно написать функцию, которая принимает произвольное число параметров произвольных типов и возвращает их сумму. Если суммирование для какой-нибудь пары типов не определено, то получим ошибку компиляции.
Почему в примере выше для организации выхода из рекурсии определён вариант sum, принимающий один параметр, а не вариант sum, принимающий нуль параметров и возвращающий число нуль?
Итак, для определения шаблона с переменным числом параметров необходимо указать пакет типов в template-заголовке:
Возможны комбинации с обычными параметрами (как в примере с суммой), но допустим максимум один пакет. Пакет может быть пустым. Узнать размер пакета можно с помощью sizeof… (имя пакета).
Types можно использовать в списке параметров функции:
Теперь parameters — это пакет параметров, типы которых задаются “параллельным” пакетом Types. Пакет параметров может быть только в функции-шаблоне, и ему обязан соответствовать пакет типов.
Пример 1: суммирование целых чисел во время компиляции
Рассмотрим простенький пример шаблона шаблона класса с переменным числом параметров. На этот раз параметры шаблона не будут типами, а будут значениями типа size_t.
Похожий код можно записать на каком-нибудь функциональном языке программирования, например, Haskell:
Зачем сразу три определения?
говорит компилятору, что у нас есть шаблонный класс Sum, параметрами которого является пакет значений типа size_t.
является специализацией шаблона Sum для случая пустого пакета параметров. В этом случае мы определяем вложенную константу value со значением 0 (enum используется для краткости записи).
также является специализацией шаблона Sum, но теперь для случая наличия хотя бы одного параметра (first). Остальные параметры собираются в пакет other, который может быть пуст. Это определение позволяет собственно “запустить” рекурсивный процесс вычисления суммы. Рано или поздно other окажется пустым пакетом и будет подставлен нуль, определённый в предыдущей специализации.
Теперь представим, что требуется уметь вычислять суммарный размер произвольного набора типов — сумму значений sizeof для каждого типа. На основе Sum можно определить новый шаблон Sum_sizeof:
При использовании C++14 (шаблонов переменных) можно переписать весь код выше более кратко:
Пример 2: println
Определим две функции: print, выводящую подряд все свои параметры в cout, и println, переадресующую вызов print и затем печатающую символ перевода строки. Шаблоны функций в C++ нельзя специализировать частично (как классы), но можно перегружать. При вызове компилятор должен выбрать наиболее конкретный из подходящих вариантов.
В данной версии определений аргументы копируются (передаются по значению), что может быть неоптимальным решением при выводе на печать строк или других больших объектов. Передачу по значению в данном случае можно заменить передачей по const-ссылке (знак & должен стоять до многоточия).
Попробуем теперь обобщить данный код — разрешим указывать объект потока вывода первым параметром. Если он не указан, то выводить в cout.
Пример 3: format
Предположим, есть набор файлов, описывающих строки интерфейса некоторого приложения на разных языках. Например, английском и русском:
Мы хотим строить фразы, подставляя в модельную фразу-шаблон конкретные слова или значения. Определим для этого функцию format, принимающую первым параметром “модель”, а прочими параметрами значения для подстановки и возвращающую строку. Пусть например, строки из файла были считаны в вектор строк phrase, тогда
даст “Intensity may take values between 0 and 1.” в случае выбора английского файла и “Насыщенность может принимать значения между 0 и 1.” в случае выбора русского файла. А
даст, соответственно “oil price forecasting” и “прогноз цены для нефти” (обратите внимание на возможность разного порядка подстановок).
Попробуем определить функцию format с помощью Стандартной библиотеки C++ и шаблонов с переменным числом параметров.
Если попытаться написать её рекурсивно в стиле print из предыдущего примера, то придётся подставлять по одной строке и организовать подсчёт номера текущей подстановки, т. е. понадобятся вспомогательные определения функции, принимающей номер. Здесь рассмотрим другой способ, а именно, создание массива строковых представлений параметров.
Теперь нет рекурсии, и нешаблонный вариант format уже не нужен. Мы инициализируем массив строк param строками, возвращаемыми для них функцией to_string. (Осторожно: to_string не принимает string! С этим разберёмся далее.). Пустая строка в начале позволяет гарантировать, что массив param будет содержать по крайней мере 1 элемент (массивы размера 0 запрещены).
Чтобы параметры-строки не вызывали ошибки компиляции, создадим свою вспомогательную функцию to_str и будем использовать её вместо to_string:
Реализовать format (собственно выполнение подстановки) можно по-разному. Например, “в лоб” на основе рукописного конечного автомата так:
Используя регулярные выражения ( #include ) из Стандартной библиотеки, можно записать похожий код более кратко:
Пример 4: список типов
Список типов type list — конструкция времени компиляции, задающая последовательность (“список”) произвольных типов и позволяющая оперировать этой последовательностью как единым целым.
Начнём с простейшего примера. Предположим, мы хотим подсчитать, сколько раз встречается заданный тип в пакете. Это можно сделать так (C++14):
Здесь, однако, нельзя говорить о списке типов Types, поскольку пакет параметров шаблона не является типом, и возможности его использования довольно ограничены. Впрочем, благодаря механизму частичной специализации шаблонов с подстановкой по образцу, легко связать пакет с конкретным типом. Для этого определим шаблон Type_list (который мог бы вовсе не иметь членов):
После этого следующий код выведет 0 2 :
Помимо подсчёта числа вхождений можно абсолютно аналогично через рекурсивное определение реализовать поиск первого вхождения (индекс):
Теперь предположим, что надо, наоборот, по номеру в списке выбрать из него тип. Это тоже можно определить рекурсивно, но уже не как переменную, а как тип. Обычно это делается через определение структуры с вложенным типом type, который и получает соответствующее значение.
В Стандартной библиотеке (в ) есть множество похожих определений, позволяющих выполнять ряд манипуляций с типами.
Метафункцией называется отображение в множество типов или из множества типов.
Конструкции вроде t_at, t_at_t или даже t_index выше можно считать метафункциями, но синтаксически затруднительно использовать их сами в качестве аргументов других метафункций, что ослабляет комбинативную мощность подобных конструкций.
“Полноценной” метафункцией в синтаксисе C++ можно назвать нешаблонный класс, имеющий вложенное определение шаблона типа, которое уже можно использовать как t_at. Это позволяет передавать метафункцию как обычный тип.
Метафункцией высшего порядка называется метафункция, принимающая другие метафункции как аргументы.
Например, определим метафункцию добавления элемента в начало списка типов.
Недостаток этой конструкции очевиден: уж очень навороченно выглядит “вызов” метафункции:
В принципе, к каждой определяемой метафункции можно добавлять аналогичное парное определение синонима типа через using, но если метафункция пришла как параметр, то мы не знаем её имя и будем вынуждены писать полный синтаксис применения.
Другой вариант состоит в определении макроса, раскрывающегося в эту идиоматическую конструкцию:
Теперь можно записывать более сложные конструкции с меньшим синтаксическим шумом. Например, рекурсивное определение метафункции высшего порядка T_map, порождающей список результатов для заданной метафункции Map (“отображение”) и списка аргументов:
Можно ли T_map записать более кратко без рекурсии?
Определим ещё одну метафункцию высшего порядка — свёртку (fold). Пример свёрток: сумма элементов, произведение элементов, максимум, минимум и т.п. Аргументами свёртки являются ассоциативная бинарная операция и список.
В случае, если аргументы метафункции должны быть значениями, а не типами (наиболее типичные случаи: булевские и целочисленные константы), то их удобно “обернуть” в тип и представлять далее как типы. Например, для целочисленных констант можно было бы определить обёртку вроде
Впрочем, в Стандартной библиотеке уже есть аналогичный шаблон integral_constant, первый параметр которого — тип константы, второй — сама константа.
Определим функцию “максимум”, принимающую аргументы integral_constant:
Определим функцию T_sizeof, преобразующую тип в его размер (в виде соответствующего integral_constant).
Теперь мы можем найти максимальный размер типа из заданного списка:
Пример 5: Variant
В наследство от C языку C++ достался ряд низкоуровневых средств. Одним из таких средств является “объединение” (union) — набор значений, размещённых по одному адресу в памяти.
Объединение никак не решает вопрос определения того, какое именно поле используется в данный момент. Однако в C++ можно реализовать шаблон, являющийся типобезопасным аналогом объединения — вариантный тип. Объект вариантного типа в каждый момент времени хранит значение одного из заданных типов (параметры шаблона) и “знает”, какого типа значение хранится в нём в данный момент.
Позаимствуем часть кода из предыдущего примера (список типов).
Здесь для простоты метафункции высшего порядка общего вида использовать не будем: вычисление максимума sizeof и alignof будем выполнять с помощью рекурсивных определений шаблонов переменных.
Можно ли записать эти определения более кратко и без рекурсии?
Проверка “является ли список 1 подмножеством списка 2” (квадратичный рекурсивный алгоритм времени компиляции):
Вспомогательная конструкция: найти в списке типов первый тип (вернуть его номер), для которого определён конструктор, принимающий заданный набор параметров Args. Возвращает –1, если такого типа в списке нет:
Теперь есть достаточно вспомогательных определений, чтобы сделать простенькую версию вариантного типа. (Более продвинутый вариант, к тому же не требующий C++14, есть в пакете библиотек Boost, аналог планируется включить в Стандартную библиотеку C++17.)
Пакет Types (и отвечающий ему список типов types) содержит типы, значения которых могут хранится в объекте Variant. Потребуем, чтобы их было не меньше 0, но и не больше 63 (в принципе, текущая реализация допускает до 255 типов).
Память выделяется статически в виде некоего “объекта” (просто кусок памяти нужного размера с нужным выравниванием) _stor. Номер типа текущего значения хранится в поле _type. Для “неинициализированных” Variant это –1, т.е. никакого значения нет. Такие объекты Variant будем называть пустыми.
Теперь представим, что нам нужно уничтожить значение, хранящееся в Variant. Иными словами, надо вызвать деструктор для того типа, номер которого записан в поле _type. Это легко реализовать, если обернуть вызовы деструкторов для всех Types в функции с общим интерфейсом, указатели на которые можно просто сохранить в массиве и выбирать их, используя _type в качестве индекса.
Для инициализации/присваивания объектов Variant данный трюк несложно повторить. Чтобы не усложнять код, реализуем минимальный рабочий вариант на основе конструктора копирования.
Можно определить также универсальный конструктор, принимающий любой набор параметров и пытающийся подобрать первый тип из списка, который допускает инициализацию таким набором параметров. Ему не нужна таблица функций-обёрток, поскольку он выбирает нужный тип статически.
Наконец, можно определить оператор присваивания одной специализации Variant значения другой специализации Variant. Для этого нам понадобится таблица перевода значений _type:
Удовлетворяет ли наш класс Variant правилу пяти?
При работе с подобными полиморфными объектами может быть удобно состыковать их с встроенными средствами C++ RTTI (run-time type information). В частности, можно реализовать извлечение результата typeid для хранимого в Variant значения. Это опять же можно сделать с помощью заранее созданной таблицы.
Для простоты кодирования возможный результат функтора-посетителя отбрасывается.
Функции с переменным количеством параметров
Здравствуйте, nap2k, Вы писали:
N>Как в С++ реализуется сабж (типа как в sprintf())
Почитайте это.
Язык C++ вслед за С позволяет писать функции с переменным числом параметров. Одним из простых примеров может служить функция, вычисляющая среднее арифметическое своих аргументов. Другой уже классический пример — функция сцепления произвольного количества строк, которая является естественным обобщением функции сцепления двух строк.
Переменный список параметров задается в заголовке функции многоточием:
int f(…)
Этот заголовок не вызывает у компилятора протестов. Такая запись означает, что при определении функции компилятору неизвестны ни количество параметров, ни их типы, и он, естественно, не может ничего проверить. Количество параметров и их типы становятся известными только при вызове функции.
Однако у программиста с написанием таких функций сразу возникают проблемы. Ведь имена параметров отсутствуют! Поэтому доступ можно осуществить только одним способом – косвенным, используя указатель. Вспомним, что все параметры при вызове помещаются в стек. Если мы каким-то образом установим указатель на начало списка параметров в стеке, то, манипулируя с указателем, мы, в принципе, можем «достать» все параметры!
Таким образом, список параметров совсем пустой быть не может, должен быть прописан хотя бы один явный параметр, адрес которого мы можем получить при выполнении программы. Заголовок такой функции может выглядеть так:
int f(int k. )
Ни запятая, ни пробел после параметра не обязательны, хотя можно их и прописать.
Есть одно обстоятельство, которое ограничивает применение таких функций: при написании функции с переменным числом параметров помимо алгоритма обработки программист должен разрабатывать и алгоритм доступа к параметрам. Так что список необъявленных параметров не может быть совсем уж произвольным – в языке C++ не существует универсальных средств распознавания элементов этого списка. Это же означает, что передача аргумента не того типа, который задумывался, или не тем способом, который подразумевался при разработке, приведет к катастрофическим последствиям – компилятор-то ничего не проверяет.
Попробуем написать функцию, вычисляющую среднее арифметическое своих аргументов. Для этого требуется решить несколько проблем
как установиться на список параметров в стеке;
как «перебирать» параметры;
как закончить перебор.
Для доступа к списку параметров нам потребуется указатель, значением которого будет адрес последнего явного параметра в списке. Ответ на второй вопрос очевиден – надо изменять значение этого указателя, чтобы переместиться на следующий параметр. Отсюда следует, что указатель должен быть типизированным, поскольку с бестиповым указателем нельзя выполнять арифметические операции. Это же означает, что программист при разработке функции с переменным числом параметров должен отчетливо себе представлять типы аргументов, которые будет обрабатывать функция. Кроме того, способ передачи параметров должен быть одинаковым для всех параметров: либо все – по значению, либо все – по ссылке, либо все – по указателю.
Ответ на последний вопрос не вызывает затруднений. Это можно сделать одним из двух способов:
явно передать среди обязательных параметров количество аргументов;
добавить в конец списка аргумент с уникальным значением, по которому будет определяться конец списка параметров;
И тот, и другой способ имеют право на жизнь — все определяется потребностями задачи и вкусами программиста. В данном случае сначала попробуем второй способ: последним значением списка параметров будет ноль (листинг 7.7).
Листинг 7.7. Вычисление среднего арифметического аргументов (ноль в конце)
Вызов такой функции может выглядеть таким образом:
double y = f(1.0, 2.0, 3.0, 4.0, 0.0);
Переменная y получит значение 2.5. Так как компилятор ничего не проверяет, то попытка вызвать такую функцию с целыми аргументами f(1,2,3,0) либо вызовет аварийный останов программы (это лучший вариант), либо в приводит к неверному (но правдоподобному — в этом главная опасность) результату.
Реализация функции, которая в качестве первого параметра получает количество аргументов, на первый взгляд, не вызывает затруднений. Однако, если первый аргумент – целое число, то требуется преобразование указателя. И тут не все варианты проходят. Не будет работать такой вариант:
Причина кроется в том, что изменение указателя производится на столько байт, сколько в памяти занимает базовый тип. В обоих случаях мы установились не на начало списка double-параметров, а на sizeof(int) байтов «раньше» — на целую переменную. И от этого адреса происходит изменение указателя на 8 байт (sizeof(double)), что приводит к совершенно неверным результатам. Решение заключается в том, чтобы сначала изменить «целый» указатель, а потом уже его преобразовать в double *. Так всегда необходимо делать, если тип первого параметра отличается от типов отсутствующих параметров (листинг 7.8).
Листинг 7.8. Вычисление среднего арифметического аргументов (количество)