В C/C++ есть понятие variadic: функция или макрос, которая может принимать переменное количество аргументов - variadic arguments, они задаются многоточием .... Есть несколько реализаций, в данном уроке рассмотрим их все.
Variadic функции #
Обычные функции поддерживают variadic, это доступно в C и C++ со стандартной библиотекой <stdarg.h>. В такой функции должен быть хотя бы один явный аргумент (первый), синтаксис выглядит так:
int foo(int count, ...); // один явный
int foo(int count...); // так тоже можно
int foo(...); // так нельзя (нет явных)
int foo(int count, int a, int b, ...); // три явных
Чтение аргументов #
Внутри функции неизвестно, сколько аргументов в неё передано, поэтому обычно это количество передаётся первым аргументом. Либо может быть передано неявно - как в форматированном выводе, который тоже является variadic-функцией. Типы variadic аргументов также неизвестны и не проверяются компилятором - функция должна либо ожидать конкретные типы, либо они задаются явно, как в том же форматированном выводе.
Для получения значений аргументов внутри функции используется набор макросов, который позволяет перебрать аргументы по одному слева направо:
va_list- тип данных для хранения перебора аргументовva_start(list, last_arg)- инициализация перебора, передаётся созданныйva_listи последний явный аргумент функцииva_arg(list, T)- получение следующего аргумента, передаётся созданныйva_listи тип данных этого аргументаva_end(list)- завершение перебора
Важно соблюдать порядок - вызывается va_start, после чего читаются все аргументы через va_arg, затем вызывается va_end
Переданные в функцию аргументы приводятся в соответствии со стандартным приведением типов (default argument promotions):
float->doublecharиshort->intlongиlong long- остаются без изменений
Именно эти типы должны быть указаны в макросе va_arg, иначе будет ошибка компиляции.
Напишем пример функции, которая суммирует любое (указанное первым аргументом) количество int аргументов:
long sum(int count, ...) {
va_list args; // перебор
va_start(args, count); // старт перебора
long total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(args, int); // получаем следующий
}
va_end(args); // конец перебора
return total;
}
unsigned char a = 100;
short b = 200;
sum(3, a, b, 300); // == 600 (a и b будут повышены до int)
sum(4, 100, 100, 100, 100); // == 400
Если передать в эту функцию long-число, например sum(3, 100L, 200, 300) - она сломается (при 2-байтном int), т.к. внутри мы читаем все аргументы как тип int и адреса данных сместятся. Если нужна версия для суммирования long или float - её нужно сделать отдельно, она будет принимать только эти типы:
long sumL(int count, ...) {
va_list args;
va_start(args, count);
long total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(args, long); // long
}
va_end(args);
return total;
}
float sumF(int count, ...) {
va_list args;
va_start(args, count);
float total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(args, double); // double
}
va_end(args);
return total;
}
sumL(3, 100L, 200L, 300L); // == 600
sumL(4, 100L, 100L, 100L, 100L); // == 400
sumF(3, 100.0f, 200.0, 300.0); // == 600.0 (float повышается до double)
sumF(4, 100.0, 100.0, 100.0, 100.0); // == 400.0
Давайте для примера сделаем аналог printf функции для Arduino, которая сможет собрать форматированную String-строку из данных разного типа. Функция будет принимать список аргументов и строку, содержащую маркеры вида $x, где x - тип аргумента (например i - int, f - float, l - long). Маркеры будут заменены на значения аргументов по порядку, а их количество и указанные типы должны соответствовать количеству и типу переданных аргументов:
String format(const char* str, ...) {
String res;
va_list args;
va_start(args, str);
while (*str) {
if (*str == '$') {
++str;
switch (*str) {
case 'i': res += va_arg(args, int); break;
case 'f': res += va_arg(args, double); break;
case 'l': res += va_arg(args, long); break;
}
} else {
res += *str;
}
++str;
}
va_end(args);
return res;
}
format("pi == $f!", 3.14);
// pi == 3.14!
format("number=$i, byte=$i, int=$i, float=$f, long=$l", 123, u8, i, f, l);
// number=123, byte=12, int=123, float=3.14, long=123456
Передача списка в функцию #
Variadic аргументы можно передать в другую функцию, но только в виде va_list - эта функция должна принимать va_list и работать с ним только через va_arg, а первая функция должна создать этот va_list и начать-закончить перебор:
// принимает va_list и количество, делает перебор
long sumList(int count, va_list args) {
long total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(args, int);
}
return total;
}
// принимает variadic, создаёт список и начинает-заканчивает перебор
long sum(int count, ...) {
va_list args;
va_start(args, count);
long result = sumList(count, args);
va_end(args);
return result;
}
Именно так и работают v-функции форматированного вывода - vprintf, vfprintf и vsnprintf.
Variadic шаблоны #
В C++11 появились шаблоны, в которых тоже есть механизм variadic аргументов:
template<typename... Args>
void foo(Args... args) {
// args - список параметров (parameter pack)
}
Раскрыть список и получить значения аргументов можно двумя способами: через рекурсию (C++11) и через fold-expression (C++17).
Через рекурсию #
Механизм работает следующим образом: список аргументов передаётся рекурсивно в указанную функцию и с каждым вызовом от него отделяется первый (левый) аргумент. Для завершения рекурсии нужна перегруженная версия этой функции без параметров:
void foo() {
// последний вызов
}
template <typename T, typename... Rest>
void foo(T first, Rest... rest) {
// first - параметры по порядку с каждым следующим вызовом
foo(rest...); // рекурсивное раскрытие
}
В отличие от va_-макросов, шаблонный variadic сохраняет тип аргументов, т.е. компилятор сделает отдельные версии функций для всех уникальных типов. А также здесь не нужно передавать количество аргументов - компилятор сделает всё сам. Это сильно упрощает дальнейшую работу, рассмотрим на примере универсальной функции вывода в монитор порта для Arduino:
void print() {
Serial.println(); // перевод строки в конце
}
template <typename T, typename... Rest>
void print(T first, Rest... rest) {
// Serial.print() имеет перегрузки на все типы данных
Serial.print(first);
print(rest...);
}
print("hello", ',', 123, " world=", 3.14);
// hello,123 world=3.14
print(123, 456, 789);
// 123456789
Для создания функций, которые должны использовать все аргументы в рамках одного действия, придётся немного подумать - как и в случае с обычными рекурсивными функциями. Например - функция сложения любого количества аргументов:
int sum() { return 0; }
template <typename T, typename... Rest>
int sum(T first, Rest... rest) {
return first + sum(rest...);
}
sum(uint8_t(100), 200, 300ul, 400.123); // == 1000
В отличие от va_-макросов, здесь можно использовать данные разных типов вперемешку - компилятор сам с ними разберётся, в данном случае это будет сложение с int - а оно "перегружено" для всех типов данных.
Через fold-expression #
В C++17 появился более удобный механизм раскрытия списка аргументов - fold-expression (свёртка).
Интересный момент: свёртка работает в некоторых компиляторах на более старых версиях, например для классических AVR Arduino (Nano, UNO...) - там C++11, но fold работает, хоть и выдаёт предупреждение
Для раскрытия используется специальный синтаксис: круглые скобки, внутри которых располагается многоточие ... и унарный оператор (обозначим как op), который будет применяться к аргументам:
(args op ... )- правая свёртка(... op args)- левая свёртка(args op ... op x)- бинарная правая свёртка (x- любое выражение, будет добавлено в свёртку)(x op ... op args)- бинарная левая свёртка
Например, функция для суммирования аргументов может выглядеть как:
template <typename... Args>
int sum(Args... args) {
return (args + ...); // оператор +
}
И развернётся в
((arg1 + arg2) + arg3) + ...
А (f(args), ...) с оператором "запятая" , раскроется в (f(arg1), f(arg2), f(arg3), ... ) и функцию для вывода в монитор порта Arduino можно записать как:
template <typename... Args>
void print(Args... args) {
(Serial.print(args), ...); // оператор ,
Serial.println();
}
// или внести перенос строки в свёртку
template <typename... Args>
void print(Args... args) {
(Serial.print(args), ..., Serial.println());
}
Fold-expression быстрее компилируется, компактнее выглядит и лучше читается.
Variadic макросы #
В #define-макрос тоже можно передавать любое количество аргументов, если он принимает их как ...:
#define FOO(...)
Внутри макроса этот список аргументов будет заменён константой __VA_ARGS__, т.е.
#define FOO(...) func(__VA_ARGS__)
FOO(1, 2, 3)
Просто развернётся в func(1, 2, 3). Это можно использовать для проброса аргументов в variadic-функции, например:
#define LOG(fmt, ...) printf(fmt, __VA_ARGS__)
Для перебора аргументов в языке нет готовых инструментов, но при помощи хитрого набора макросов его можно реализовать, например - моя библиотека FOR_MACRO (описание с примерами есть по ссылке). По умолчанию она поддерживает списки до 512 элементов, а в комплекте идёт Python-скрипт, при помощи которого можно сгенерировать макрос с поддержкой другого максимального количества аргументов.
Библиотека может пригодиться при метапрограммировании и автоматической генерации кода, т.к. variadic-макрос работает именно с макросами и с его помощью можно делать очень интересные вещи, например - создание строк:
// вспомогательные макросы
#define MF6(N, i, p, val) const char* p##_##i = #val;
#define FOR_6(name, ...) FOR_MACRO(MF6, name, __VA_ARGS__)
// использование
FOR_6(strings, test, kek, string);
// развернётся в:
// const char* strings_2 = "test";
// const char* strings_1 = "kek";
// const char* strings_0 = "string";
Полезные страницы #
- Набор GyverKIT – наш большой стартовый набор Arduino, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])
- Поддержать автора за работу над уроками