Przekazywanie zmiennej liczby argumentów

333

Powiedzmy, że mam funkcję C, która pobiera zmienną liczbę argumentów: Jak mogę wywołać inną funkcję, która oczekuje od niej zmiennej liczby argumentów, przekazując wszystkie argumenty, które dostały się do pierwszej funkcji?

Przykład:

void format_string(char *fmt, ...);

void debug_print(int dbg_lvl, char *fmt, ...) {
    format_string(fmt, /* how do I pass all the arguments from '...'? */);
    fprintf(stdout, fmt);
 }
Vicent Marti
źródło
4
Twój przykład wygląda mi trochę dziwnie, ponieważ przekazujesz fmt zarówno do format_string (), jak i do fprintf (). Czy format_string () powinien jakoś zwrócić nowy ciąg?
Kristopher Johnson
2
Przykład nie ma sensu. Chodziło tylko o pokazanie obrysu kodu.
Vicent Marti,
162
„należy googlować”: nie zgadzam się. Google ma dużo hałasu (niejasne, często mylące informacje). Posiadanie dobrej (przegłosowanej, zaakceptowanej odpowiedzi) na stackoverflow naprawdę pomaga!
Ansgar
71
Po prostu ważyłem: przyszedłem do tego pytania z Google, a ponieważ było to przepełnienie stosu, byłem bardzo pewny, że odpowiedź będzie przydatna. Więc spytaj!
tenpn
32
@Ilya: jeśli nikt nigdy nie spisałby rzeczy poza Google, nie byłoby żadnych informacji do wyszukiwania w Google.
Erik Kaplun,

Odpowiedzi:

211

Aby przekazać elipsy, musisz przekonwertować je na listę va_list i użyć tej listy va w swojej drugiej funkcji. Konkretnie;

void format_string(char *fmt,va_list argptr, char *formatted_string);


void debug_print(int dbg_lvl, char *fmt, ...) 
{    
 char formatted_string[MAX_FMT_SIZE];

 va_list argptr;
 va_start(argptr,fmt);
 format_string(fmt, argptr, formatted_string);
 va_end(argptr);
 fprintf(stdout, "%s",formatted_string);
}
SmacL
źródło
3
Kod pochodzi z pytania i jest tak naprawdę ilustracją konwersji elips, a nie jakiejkolwiek funkcji. Jeśli spojrzysz na to, format_stringrównież nie będzie to przydatne, ponieważ musiałoby to modyfikować fmt in situ, czego z pewnością nie należy robić. Opcje obejmowałyby całkowite pozbycie się format_string i użycie vfprintf, ale to czyni założenia o tym, co faktycznie robi format_string, lub format_string zwraca inny ciąg. Przeredaguję odpowiedź, aby pokazać to drugie.
SmacL
1
Jeśli twój ciąg formatu używa takich samych poleceń ciągu formatu jak printf, możesz także uzyskać kompilatory, takie jak gcc i clang, aby wyświetlały ostrzeżenia, jeśli ciąg formatu nie jest zgodny z przekazanymi argumentami. Zobacz atrybut funkcji GCC ' format ”, aby uzyskać więcej informacji: gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html .
Doug Richardson
1
Nie wydaje się to działać, jeśli dwa razy z rzędu podajesz argumenty.
fotanus
2
@fotanus: jeśli wywołujesz funkcję za pomocą, argptra wywoływana funkcja używa argptrw ogóle, jedyną bezpieczną rzeczą do zrobienia jest wywołanie, va_end()a następnie ponowne uruchomienie w va_start(argptr, fmt);celu ponownej inicjalizacji. Lub możesz użyć, va_copy()jeśli twój system go obsługuje (C99 i C11 tego wymagają; C89 / 90 nie wymaga).
Jonathan Leffler,
1
Uwaga: komentarz @ ThomasPadron-McCarthy jest już nieaktualny, a końcowy plik fprintf jest w porządku.
Frederick
59

Nie ma możliwości wywołania (np.) Printf bez wiedzy, ile argumentów mu przekazujesz, chyba że chcesz dostać się do niegrzecznych i nieprzenośnych sztuczek.

Powszechnie stosowanym rozwiązaniem jest, aby zawsze zapewnić alternatywną postać funkcji vararg, więc printfma vprintfco zajmuje va_listw miejsce .... The... wersje są tylko obwolut wokół va_listwersjach.


źródło
Pamiętaj, że nievsyslog jest zgodny z POSIX .
patryk.beza
53

Funkcje Variadic mogą być niebezpieczne . Oto bezpieczniejsza sztuczka:

   void func(type* values) {
        while(*values) {
            x = *values++;
            /* do whatever with x */
        }
    }

func((type[]){val1,val2,val3,val4,0});
Rose Perrone
źródło
11
Jeszcze lepsza jest ta sztuczka: #define callVardicMethodSafely(values...) ({ values *v = { values }; _actualFunction(values, sizeof(v) / sizeof(*v)); })
Richard J. Ross III
5
@ RichardJ.RossIII Chciałbym, żebyś rozwinął swój komentarz, jest on trudny do odczytania w ten sposób, nie mogę zrozumieć idei kodu i faktycznie wygląda bardzo interesująco i użytecznie.
penelopa
5
@ArtOfWarfare Nie jestem pewien, czy zgadzam się, że to zły hack, Rose ma świetne rozwiązanie, ale wymaga pisania func ((type []) {val1, val2, 0}); co wydaje się niezgrabne, gdybyś miał # zdefiniować func_short_cut (...) func ((type []) { VA_ARGS }); wtedy możesz po prostu wywołać func_short_cut (1, 2, 3, 4, 0); co daje tę samą składnię, co normalna funkcja variadyczna, z dodatkową zaletą zgrabnej sztuczki Rose ... o co tu chodzi?
chrispepper1989
9
Co jeśli chcesz przekazać 0 jako argument?
Julian Gold,
1
Wymaga to od użytkowników pamiętania, aby zadzwonić z numerem kończącym 0. Jak to jest bezpieczniejsze?
cp.engr
29

We wspaniałym C ++ 0x możesz użyć różnych szablonów:

template <typename ... Ts>
void format_string(char *fmt, Ts ... ts) {}

template <typename ... Ts>
void debug_print(int dbg_lvl, char *fmt, Ts ... ts)
{
  format_string(fmt, ts...);
}
user2023370
źródło
Nie zapominaj, że różne szablony nie są jeszcze dostępne w Visual Studio ... to oczywiście może nie dotyczyć Ciebie!
Tom Swirly,
1
Jeśli korzystasz z programu Visual Studio, szablony różnorodne można dodać do programu Visual Studio 2012 za pomocą CTP z listopada 2012 r. Jeśli korzystasz z programu Visual Studio 2013, będziesz mieć różne szablony.
user2023370,
7

Można użyć złożenia wbudowanego do wywołania funkcji. (w tym kodzie zakładam, że argumentami są znaki).

void format_string(char *fmt, ...);
void debug_print(int dbg_level, int numOfArgs, char *fmt, ...)
    {
        va_list argumentsToPass;
        va_start(argumentsToPass, fmt);
        char *list = new char[numOfArgs];
        for(int n = 0; n < numOfArgs; n++)
            list[n] = va_arg(argumentsToPass, char);
        va_end(argumentsToPass);
        for(int n = numOfArgs - 1; n >= 0; n--)
        {
            char next;
            next = list[n];
            __asm push next;
        }
        __asm push fmt;
        __asm call format_string;
        fprintf(stdout, fmt);
    }
Yoda
źródło
4
Nie przenośny, zależy od kompilatora i zapobiega optymalizacji kompilatora. Bardzo złe rozwiązanie.
Geoffroy
4
Nowe również bez usuwania.
user7116
8
Przynajmniej to faktycznie odpowiada na pytanie, bez redefiniowania pytania.
lama12345,
6

Możesz także spróbować makra.

#define NONE    0x00
#define DBG     0x1F
#define INFO    0x0F
#define ERR     0x07
#define EMR     0x03
#define CRIT    0x01

#define DEBUG_LEVEL ERR

#define WHERESTR "[FILE : %s, FUNC : %s, LINE : %d]: "
#define WHEREARG __FILE__,__func__,__LINE__
#define DEBUG(...)  fprintf(stderr, __VA_ARGS__)
#define DEBUG_PRINT(X, _fmt, ...)  if((DEBUG_LEVEL & X) == X) \
                                      DEBUG(WHERESTR _fmt, WHEREARG,__VA_ARGS__)

int main()
{
    int x=10;
    DEBUG_PRINT(DBG, "i am x %d\n", x);
    return 0;
}
GeekyJ
źródło
6

Chociaż można rozwiązać przekazanie formatera, najpierw przechowując go w buforze lokalnym, ale wymaga to stosu i może czasem być problem. Próbowałem śledzić i wydaje się, że działa dobrze.

#include <stdarg.h>
#include <stdio.h>

void print(char const* fmt, ...)
{
    va_list arg;
    va_start(arg, fmt);
    vprintf(fmt, arg);
    va_end(arg);
}

void printFormatted(char const* fmt, va_list arg)
{
    vprintf(fmt, arg);
}

void showLog(int mdl, char const* type, ...)
{
    print("\nMDL: %d, TYPE: %s", mdl, type);

    va_list arg;
    va_start(arg, type);
    char const* fmt = va_arg(arg, char const*);
    printFormatted(fmt, arg);
    va_end(arg);
}

int main() 
{
    int x = 3, y = 6;
    showLog(1, "INF, ", "Value = %d, %d Looks Good! %s", x, y, "Infact Awesome!!");
    showLog(1, "ERR");
}

Mam nadzieję że to pomoże.

VarunG
źródło
2

Rozwiązanie Rossa zostało trochę oczyszczone. Działa tylko wtedy, gdy wszystkie argumenty są wskaźnikami. Również implementacja języka musi obsługiwać wyświetlanie poprzedniego przecinka, jeśli __VA_ARGS__jest pusty (zarówno Visual Studio C ++, jak i GCC do).

// pass number of arguments version
 #define callVardicMethodSafely(...) {value_t *args[] = {NULL, __VA_ARGS__}; _actualFunction(args+1,sizeof(args) / sizeof(*args) - 1);}


// NULL terminated array version
 #define callVardicMethodSafely(...) {value_t *args[] = {NULL, __VA_ARGS__, NULL}; _actualFunction(args+1);}
BSalita
źródło
0

Załóżmy, że masz typową funkcję wariadyczną, którą napisałeś. Ponieważ przed argumentem variadycznym wymagany jest co najmniej jeden argument ..., zawsze musisz napisać dodatkowy argument w użyciu.

Czy ty

Jeśli zawijasz funkcję variadic w makrze, nie potrzebujesz poprzedzającego argumentu. Rozważ ten przykład:

#define LOGI(...)
    ((void)__android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__))

Jest to oczywiście znacznie wygodniejsze, ponieważ nie trzeba za każdym razem określać początkowego argumentu.

Inżynier
źródło
-5

Nie jestem pewien, czy to działa dla wszystkich kompilatorów, ale do tej pory działało dla mnie.

void inner_func(int &i)
{
  va_list vars;
  va_start(vars, i);
  int j = va_arg(vars);
  va_end(vars); // Generally useless, but should be included.
}

void func(int i, ...)
{
  inner_func(i);
}

Możesz dodać ... do inner_func (), jeśli chcesz, ale nie potrzebujesz. Działa, ponieważ va_start używa adresu danej zmiennej jako punktu początkowego. W tym przypadku podajemy mu odwołanie do zmiennej w func (). Używa więc tego adresu i odczytuje zmienne później na stosie. Funkcja inner_func () odczytuje z adresu stosu func (). Działa to tylko wtedy, gdy obie funkcje używają tego samego segmentu stosu.

Makra va_start i va_arg będą na ogół działać, jeśli podasz im dowolny var jako punkt początkowy. Więc jeśli chcesz, możesz przekazać wskaźniki do innych funkcji i również z nich korzystać. Możesz łatwo tworzyć własne makra. Wszystkie makra to adresy pamięci typecast. Jednak zmuszanie ich do działania dla wszystkich kompilatorów i konwencji wywoływania jest denerwujące. Ogólnie łatwiej jest korzystać z tych, które są dostarczane z kompilatorem.

Jim
źródło