Посмотр рубрик

variadic

В 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 -> double
  • char и short -> int
  • long и 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";

Полезные страницы #

Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх