с функция с неопределенным числом параметров
Функции с переменным числом параметров
Функции с переменным числом параметров
К ак уже обсуждалось ранее, по умолчанию параметры передаются функции через стек. Поэтому, технически, нет ограничения на количество передаваемых параметров – “запихать” можно сколько угодно. Проблема в том, как потом функция будет разбирать переданные параметры. Функции с переменным числом параметров объявляются как обычные функции, но вместо недостающих аргументов ставится многоточие. Пусть мы хотим сделать функцию, которая складывает переданные ей числа, чисел может быть произвольное количество. Необходимо каким-то образом передать функции число параметров. Во-первых, можно явно передать число параметров обязательным аргументом. Во-вторых, последний аргумент может иметь некоторое «терминальное» значение, наткнувшись на которое функция закончит выполнение.
Общий принцип работы следующий: внутри функции берём указатель на аргумент, далее двигаемся к следующему аргументу, увеличивая значение указателя.
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.
С++ для начинающих Функции с переменным числом параметров
Для того, чтобы получить доступ ко всем параметрам, принимаемых функцией, нужно знать имя и тип хотя бы одного параметра.
Вот такой код не вызывает возражений, а многоточие после первого параметра внутри параметров функции обозначает, что в функцию можно передать для обработки более одного параметра.
Должен возникнуть вопрос как же получить доступ к каждому из этих параметров. Для того, чтоб хорошо понять то что будет написано, требуется знание указателей. Чтобы иметь доступ к каждому из параметров, нужно знать адрес первого параметра, а чтобы взять этот адрес, как раз и требуется указатель
=========
Что можно увидеть? Первым делом внутри функции происходят взятие адреса первого параметра, при этом тип указателя должен совпадать с типом этого параметра. Дальше идет простой перебор всех параметров с помощью цикла.
Таким же образом можно привести классический пример суммирования элементов
while (* P ) //Пока встречаются параметры
<
sum = sum +(* P ) ” “ ; //Прибавляем к сумме то что взяли по адресу P
P ++; //Адресная арифметика. Смена текущего адреса на следующий
>
cout sum endl ; //Вывод результата на экран
>
Вообще весь этот механизм удобно использовать тогда, когда требуется обработка однотипных элементов и чем-то всё напоминает обычный массив элементов. Учитывая, что указатель должен быть того же типа, что и элемент, расположенный по адресу на который он ссылается, можно сказать, что переменное число параметров может быть корректно использовано как раз тогда, когда все параметры передаваемые в такую функцию принадлежат одному и тому же типу (имеются ввиду те параметры, которые идут туда где расположено многоточие).
Но вообще в функцию можно передавать произвольное число параметров с переменным типом, я не говорил, что этого сделать нельзя, можете перечитать всё снова, чтобы в этом убедиться. При определении функций с переменным количеством параметров, рекомендуется использовать специальный набор макроопределений, которые становятся доступными при включении в программу заголовочного файла stdarg.h
Эти макроопределения обеспечивают простой и стандартный (независящий от реализации) способ доступа к спискам параметров переменной длины. Я не буду расписывать, так как сам плохо все это представляю, но приведу пример, содранный с учебника Марченко А.Л. Бархатный путь (2005). Там более менее расписано что к чему и нарушать его авторство я не хочу. Но кое что оттуда вынесу
С функция с неопределенным числом параметров
Здравствуйте, 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. Вычисление среднего арифметического аргументов (количество)
Мой подход к реализации делегатов в C++: вызов функции с неизвестными параметрами во время выполнения
Предыстория
Конечно, полная имитация делегатов слишком сложна, поэтому в этой статье будут продемонстрированы лишь общая архитектура библиотеки и решение некоторых важных проблем, возникающих, когда имеешь дело с тем, что не поддерживается языком напрямую.
Вызов функций с неопределённым количеством параметров и неизвестными во время компиляции типами
Конечно, это главная проблема с C++, которая решается не так уж и просто. Конечно, в C++ есть средство, унаследованное из C – varargs, и, скорее всего, это первое, что придёт на ум… Однако они не подходят, во-первых, из-за своей типонебезопасной природы (как и многие вещи из C), во-вторых, при использовании таких аргументов надо точно заранее знать, какие у аргументов типы. Впрочем, почти наверняка, это ещё не все проблемы с varargs. В общем, это средство нам здесь не помощник.
А теперь перечислю средства, которые помогли мне решить эту проблему.
std::any
Начиная с C++17, в языке появился замечательный-контейнер-хранилище для чего угодно – некое отдалённое подобие System.Object в CLI – это std::any. Этот контейнер действительно может хранить что угодно, да ещё как: эффективно! – стандарт рекомендует маленькие объекты хранить непосредственно в нём, большие уже можно хранить в динамической памяти (хотя такое поведение не является обязательным, корпорация Microsoft в своей реализации C++ так и сделала, что не может не радовать). А подобием лишь его можно назвать потому, что System.Object участвует в отношениях наследования («is a»), а std::any – участвует в отношениях принадлежности («has a»). Кроме данных, контейнер содержит указатель на объект std::type_info – RTTI о типе, объект которого «лежит» в контейнере.
Для контейнера выделен целый заголовочный файл .
Чтобы «вытащить» объект из контейнера, нужно использовать шаблонную функцию std::any_cast(), которая возвращает ссылку на объект.
Пример использования:
Если запрашиваемый тип не совпадает с тем, что имеет объект внутри контейнера, тогда выбрасывается исключение std::bad_any_cast.
Кроме классов std::any, std::bad_any_cast и функции std::any_cast, в заголовочном файле есть шаблонная функция std::make_any, аналогичная std::make_shared, std::make_pair и другим функциям этого рода.
Безусловно, практически нереально в C++ было бы реализовать динамический вызов функций без информации о типах во времени выполнения. Ведь надо же как-то проверять, правильные типы переданы, или нет.
Примитивная поддержка RTTI в C++ есть довольно давно. Вот только в том-то и дело, что примитивная – мы мало что можем узнать о типе, разве только декорированное и недекорированное имена. Кроме того, мы можем сравнивать типы друг с другом.
Шаблоны
Ещё одна чрезвычайно важная особенность языка, нужная нам для реализации нашей задумки – это шаблоны. Это средство – довольно мощное и исключительно непростое, по сути позволяет генерировать код во время компиляции.
Шаблоны – это очень обширная тема, и в рамках статьи раскрыть её не удастся, да и не нужно это. Будем считать, что читатель понимает, о чём речь. Какие-то неясные моменты будут раскрыты в процессе.
Упаковка аргументов с последующим вызовом
Итак, у нас есть некая функция, принимающая на вход несколько параметров.
Продемонстрирую набросок кода, который объяснит мои намерения.
Возможно, вы спросите – как это возможно? Название класса Variadic_args_binder подсказывает, что объект связывает функцию и аргументы, которые нужно ей передать при вызове. Таким образом, остаётся лишь вызвать этот связыватель как функцию без параметров!
Так это выглядит снаружи.
Если сразу же, не задумавшись, сделать предположение, как это можно реализовать, то на ум, возможно, придёт написать несколько специализаций Variadic_args_binder для разного количества параметров. Однако это невозможно в случае необходимости поддержки неограниченного числа параметров. И проблема вот ещё в чём: аргументы, к сожалению, нужно подставить в вызов функции статически, то есть в конечном итоге для компилятора код вызова должен свестись вот к такому:
Так устроен C++. И это всё сильно усложняет.
Тут справится лишь шаблонная магия!
Основная идея – создавать рекурсивные типы, хранящие на каждом уровне вложенности один из аргументов или функцию.
Итак, объявим класс _Tagged_args_binder:
Чтобы удобно «переносить» пакеты типов, создадим вспомогательный тип Type_pack_tag (зачем это понадобилось, скоро станет понятно):
Теперь создаём специализации класса _Tagged_args_binder.
Начальные специализации
Как известно, чтобы рекурсия не была бесконечной, необходимо определить граничные случаи.
Следующие специализации являются начальными. Для упрощения приведу специализации лишь для нессылочных типов и правосторонних ссылочных типов (rvalue reference).
Специализация для непосредственно параметров-значений:
Здесь хранятся первый аргумент вызова ap_arg и остальная часть рекурсивного объекта ap_caller_part. Обратите внимание, что тип T1 «переместился» из первого пакета типов в этом объекте во второй в «хвосте» рекурсивного объекта.
Специализация для rvalue-ссылок:
Шаблонные «правосторонние» ссылки – на самом деле не являются правосторонними значениями. Это так называемые «универсальные ссылки», которые, в зависимости от типа T1, становятся то T1&, то T1&&. Поэтому приходится использовать обходные пути: во-первых, так как определены специализации для обеих видов ссылок (не совсем корректно сказано, по уже озвученной причине) и для нессылочных параметров, при инстанцировании шаблона будет выбрана именно нужная специализация, даже если это правосторонняя ссылка; во-вторых – для передачи типа T1 из пакета в пакет используется исправленная версия move_ref_T1, превращённая в настоящую rvalue-ссылку.
Специализация с обычной ссылкой делается аналогично, с необходимыми исправлениями.
Конечная специализация
Эта специализация ответственна за хранение функционального объекта и, по сути, является обёрткой над ним. Она является завершающей рекурсивный тип.
Обратите внимание, как используется здесь Type_pack_tag. Все типы параметров теперь собраны в левом пакете. Это значит, что все они обработаны и упакованы.
Теперь, думаю, становится понятно, зачем нужно было использовать именно Type_pack_tag. Дело в том, язык не позволил бы использовать рядом два пакета типов, например, вот так:
поэтому приходится разделять их на два раздельных пакета внутри двух типов. Кроме того, надо как-то отделять обработанные типы от ещё не обработанных.
Промежуточные специализации
Из промежуточных специализаций напоследок приведу специализацию, опять-таки, для типов-значений, остальное по аналогии:
Эта специализация предназначена для упаковки любого аргумента, кроме первого.
Класс-связыватель
Класс _Tagged_args_binder не предназначен для непосредственного использования, что я хотел подчеркнуть одинарным подчёркиванием в начале его названия. Поэтому приведу код небольшого класса, являющегося своего рода «интерфейсом» к этому некрасивому и неудобному в использовании типу (в котором, однако, используются довольно необычные приёмы C++, что придаёт ему некоторый шарм, на мой взгляд):
Соглашение unihold – передача ссылок внутри std::any
Внимательный читатель наверняка заметил, что в коде используется функция unihold::reference_any_cast(). Эта функция, а также её аналог unihold::pointer_any_cast(), разработаны для реализации соглашения библиотеки: аргументы, которые необходимо передать по ссылке, передаются по указателю в std::any.
Функция reference_any_cast всегда возвращает ссылку на объект, хранится ли в контейнере сам объект или только указатель на него. Если std::any содержит в себе объект, то возвращается ссылка на этот объект внутри контейнера; если же содержит указатель – то возвращается ссылка на объект, на который указывает указатель.
Для каждой из функций есть варианты константного std::any и перегруженные версии для определения того, является ли контейнер std::any владельцем объекта или же содержит лишь указатель.
Функции нужно специализировать явно типом хранимого объекта, так же, как преобразования типов C++ и подобные им шаблонные функции.
Заключение
Я постарался кратко описать один из возможных подходов к решению проблемы динамического вызова функций на C++. Впоследствии это ляжет в основу библиотеки делегатов на C++ (на самом деле, я уже написал основной функционал библиотеки, а именно, полиморфные делегаты, но библиотеку ещё нужно переписать, как подобает, чтобы можно было демонстрировать код, и добавить некоторый нереализованный функционал). В ближайшем будущем я планирую закончить работу над библиотекой и рассказать, как именно я реализовал остальной функционал делегатов на C++.
Функция с неопределенным количеством параметров (без использования stdarg.h)
Мне нужно создать функцию, которая считает выражение:
4 ответа 4
Как я уже сказал выше в комментариях под вопросом, написать функцию с переменным количеством аргументов без использования средств, предоставляемых заголовочным файлом переносимым способом невозможно. Имейте это в виду, и, если нужно, покажите этот ответ своему преподователю.
А теперь смотрите, почему это не возможно с теоретической и почти всегда невозможно c практической сторон.
Теоретическая сторона
С теоретической стороны это невозможно, т. к. этого не позволяет стандарт языка C. В стандарте сказано, что размещение параметров при вызове функции неспецифицированно:
9 Each parameter has automatic storage duration; its identifier is an lvalue. 166) The layout of the storage for parameters is unspecified.
— ISO/IEC 9899:2017 N2176 §6.9.1
Это значит, что способ передачи аргументов в функцию полностью зависит от реализации и делать какие-либо предположения о том, как параметры передаются в функцию — бессмысленно.
Практическая сторона
Теперь рассмотрим практическую сторону. Как я уже сказал, способ передачи аргументов в функцию зависит от реализации. Проблема в том, что реализаций может быть много. Очень много. Под реализацией понимается, как минимум:
Количество всех возможных комбинаций очень велико. Причем для каждой такой комбинации код, в общем случае, будет разным. Придется учитывать особенности каждого ABI, и не факт, что ABI позволит получить значения аргументов таким «грязным» способом. Вынужден признать, что в некоторых обстоятельствах мы можем решить задачу, хотя говорить о корректности решения в таких случаях не приходится.
Конкретные примеры
Допустим, что мы заранее знаем, каким компилятором, под какую архитектуру и с каким ABI будет компилироваться программа. Например, это будет GCC, Intel 64 2 и System V ABI (x86_64 psABI).
Напишем код, который вызывал бы нужную нам функцию:
И проверим нужные смещения отладчиком:
Отлично! Теперь можем написать реализацию функцию sum :
1) За подробностями обращайтесь к разделу «5.1.2 Execution Environement» стандарта C17.
2) Эту ISA часто называют x64, x86_64 или x86-64. Скорее всего, процессор вашего компьютера реализует именно эту архитектуру.
3) Эту ISA также называют x32, i386 или Intel-386.