длина-ва списке, когда через список аргументов переменной?


Есть ли способ вычислить длину va_list? Во всех примерах, которые я видел, количество переменных параметров задается явно.

6 29

6 ответов:

Нет способа вычислить длину va_list, поэтому вам нужна строка формата в printf подобных функциях.

Единственныефункции макросы, доступные для работы с va_list являются :

  • va_start - Начните использовать va_list
  • va_arg - получить следующий аргумент
  • va_end - прекратите использовать va_list
  • va_copy (начиная с C++11 и C99) - копировать va_list

Пожалуйста, обратите внимание, что вам нужно позвонить va_start и va_end в та же область, что означает, что вы не можете обернуть его в служебный класс, который вызывает va_start в своем конструкторе и va_end в своем деструкторе (я был укушен этим однажды).

Например, этот класс бесполезен:

class arg_list {
    va_list vl;
public:
    arg_list(const int& n) { va_start(vl, n); }
    ~arg_list() { va_end(vl); }
    int arg() {
        return static_cast<int>(va_arg(vl, int);
    }
};

GCC выводит следующую ошибку

T.cpp: в конструкторе arg_list::arg_list(const int&):
Строка 7: Ошибка: va_start используется в функции с фиксированными args
компиляция прекращена из-за-Wfatal-ошибок.

Один из подходов, который еще не был упомянут, заключается в использовании макроса препроцессора для вызова функции variadict, используя длину va_list в качестве первого параметра, а также вперед по аргументам. Это несколько "милое" решение,но не требует ручного ввода длины списка аргументов.

Предположим, что у вас есть следующая функция:

int Min(int count, ...) {
    va_list args;
    va_start(args, count);

    int min = va_arg(args, int);
    for (int i = 0; i < count-1; ++i) {
      int next = va_arg(args, int);
      min = min < next ? min : next;
    }
    va_end(args);

    return min;
}
Идея заключается в том, что у вас есть макрос препроцессора, способный подсчитывать количество аргументов, используя маску для __VA_ARGS__. Есть несколько хороших библиотек препроцессоров для определения длины __VA_ARGS__, включая P99 и Boost Preprocessor, но просто чтобы я не оставлял дыр в этом ответе, вот как это можно сделать:
#define IS_MSVC _MSC_VER && !__INTEL_COMPILER

/**
 * Define the macros to determine variadic argument lengths up to 20 arguments. The MSVC 
 * preprocessor handles variadic arguments a bit differently than the GNU preprocessor,
 * so we account for that here. 
 */
#if IS_MSVC
  #define MSVC_HACK(FUNC, ARGS) FUNC ARGS
  #define APPLY(FUNC, ...) MSVC_HACK(FUNC, (__VA_ARGS__))
  #define VA_LENGTH(...) APPLY(VA_LENGTH_, 0, ## __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
#else
  #define VA_LENGTH(...) VA_LENGTH_(0, ## __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
#endif

/**
 * Strip the processed arguments to a length variable.
 */
#define VA_LENGTH_(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, N, ...) N

Примечание : большая часть шума сверху - это поддержка обхода MSVC.

С выше определенным, вы можете создать один макрос для выполнения всех операций на основе длины:

/**
 * Use the VA_LENGTH macro to determine the length of the variadict args to
 * pass in as the first parameter, and forward along the arguments after that.
 */
#define ExecVF(Func, ...) Func(VA_LENGTH(__VA_ARGS__), __VA_ARGS__)

Этот макрос способен вызывать любую функцию variadict до тех пор, пока он начинается с параметра int count. Короче говоря, вместо использования:

int result = Min(5, 1, 2, 3, 4, 5);

Вы можете использовать:

int result = ExecVF(Min, 1, 2, 3, 4, 5);

Вот шаблонная версия Min, которая использует тот же подход: https://gist.github.com/mbolt35/4e60da5aaec94dcd39ca

Нет прямогоспособа для вариадической функции определить, сколько аргументов было передано. (По крайней мере, нет переносимого способа; интерфейс <stdarg.h> не предоставляет эту информацию.)

Существует несколькокосвенных способов.

Два наиболее распространенных:

  • строка формата (которая определяет, с помощью того, что можно назвать небольшим простым языком, число и тип(ы) оставшихся аргументов). Семейства функций *printf() и *scanf() используют этот механизм.
  • значение sentinel, обозначающее конец аргументов. Некоторые функции семейства Unix / POSIX exec*() делают это, используя нулевой указатель для обозначения конца аргументов.
Но есть и другие возможности:
  • проще говоря, целочисленное число, определяющее число следующих аргументов; предположительно, в этом случае все они будут одного типа.
  • переменные аргументы, где аргумент может быть значением перечисления, определяющим тип следующего аргумента. Гипотетический пример может выглядеть так::
    func(ARG_INT, 42, ARG_STRING, "foo", ARG_DOUBLE, 1.25, ARG_END);
    или даже:
    func("-i", 42, "-s", "foo", "-d", 1.25, "");
    если вы хотите эмулировать способ, которым аргументы обычно передаются командам Unix.

Можно даже присвоить значение глобальной переменной, чтобы указать количество аргументов:

func_arg_count = 3;
func(1, 2, 3);
Что было бы некрасиво, но совершенно законно.

Во всех этих техниках ответственность за передачу последовательных аргументов полностью лежит на вызывающем; вызываемый может только предположим, которые по своим параметрам являются правильными.

Обратите внимание, что функция с переменным числом аргументов не требуется обрабатывать все переданные ей аргументы. Например, это:

printf("%d\n", 10, 20);

Выведет 10 и спокойно проигнорирует 20. Редко есть какая-либо причина, чтобы воспользоваться этой функцией.

Можно попробовать использовать функцию _vscprintf, Если вы работаете в среде MS Visual Studio. Вот пример, как использовать _vscprintf, я использовал его, чтобы узнать, сколько места мне требуется для malloc для моего заголовка консоли.

int SetTitle(const char *format,...){
    char *string;
    va_list arguments;

    va_start(arguments,format);
        string=(char *)malloc(sizeof(char)*(_vscprintf(format,arguments)+1));
        if(string==NULL)
            SetConsoleTitle("Untitled");
        else
            vsprintf(string,format,arguments);
    va_end(arguments);

    if(string==NULL)
        return SETTITLE_MALLOCFAILED;
    SetConsoleTitle(string);
    free(string);
    return 0;
}

Или вы можете сделать это, добавить вывод во временный файл, а затем прочитать данные из него в выделенную память, как я сделал в следующем примере:

void r_text(const char *format, ...){
    FILE *tmp = tmpfile();
    va_list vl;
    int len;
    char *str;

    va_start(vl, format);
        len = vfprintf(tmp, format, vl);
    va_end(vl);
    rewind(tmp);
    str = (char *) malloc(sizeof(char) * len +1);
    fgets(str, len+1, tmp);
    printf("%s",str);
    free(str);
    fclose(tmp);
}

Хм, если вы не боитесь неприятного взлома asm, то вы можете использовать соглашение о вызове вашего компилятора. Однако это ограничит ваш код конкретной платформой / компилятором / соглашением о вызовах.

Например, в BDS2006 C++ 32bit x86 Windows app (я буду ссылаться только на эту платформу) аргументы помещаются в стек, затем вызываются, а затем значение указателя стека восстанавливается (по размеру используемого стека) после возврата функции. Вот небольшой пример:

double x;
x=min(10.0,20.0,30.0,40.0,50.0);

Вызов переводится так:

Unit1.cpp.28: x=min(10.0,20.0,30.0,40.0,50.0);
00401B9C 6800004940       push $40490000
00401BA1 6A00             push $00
00401BA3 6800004440       push $40440000
00401BA8 6A00             push $00
00401BAA 6800003E40       push $403e0000
00401BAF 6A00             push $00
00401BB1 6800003440       push $40340000
00401BB6 6A00             push $00
00401BB8 6800002440       push $40240000
00401BBD 6A00             push $00
00401BBF E894FDFFFF       call min(double,double,????)
00401BC4 83C428           add esp,$28
Обратите внимание на последнюю инструкцию после вызова. $28 - это размер, потребляемый 4 аргументами и одним возвращаемым значением. Таким образом, если вы можете прочитать это значение в своей функции, вы можете точно определить количество аргументов (если их размер известен). Так вот рабочий пример:
double min(double x,double ...) // = min(x,y)
        {
        int n,dn=sizeof(double);
        asm {
            mov eax,esp // store original stack pointer 
            mov esp,ebp // get to the parrent scope stack pointer
            pop ebx
            pop ebx     // this reads the return address of the call pointing to the first instruction after it which is what we want
            mov esp,eax // restore stack pointer
            sub eax,eax; // just eax=0
            mov al,[ebx+2] // read lowest BYTE of eax with the $28 from the add esp,$28
            mov n,eax // store result to local variable for usage
            }
        n-=dn;  // remove return value  from the count

        double z; z=x;
        va_list va;
        va_start(va,x); n-=dn;
        for (;n>=0;n-=dn)
            {
            x=va_arg(va,double);
            if (z>x) z=x;
            }
        va_end(va);
        return z;
        }

Будьте осторожны, каждый компилятор может иметь различную последовательность вызова, поэтому сначала проверьте список сборок во время отладки перед использованием !!!

Используйте _vscprintf для определения длины списка переменных. https://msdn.microsoft.com/en-us/library/w05tbk72.aspx