Zmienna liczba argumentów w C ++?

293

Jak napisać funkcję, która akceptuje zmienną liczbę argumentów? Czy to możliwe, jak?

nunos
źródło
49
W tej chwili z C ++ 11 odpowiedzi na to pytanie byłyby bardzo różne
K-ballo
1
@ K-ballo Dodałem również przykłady C ++ 11, ponieważ ostatnio zadałem to samo pytanie i czułem, że jest to potrzebne, aby uzasadnić zamknięcie stackoverflow.com/questions/16337459/...
Shafik Yaghmour
1
Dodałem również opcje wcześniejszej wersji C ++ 11 , więc teraz powinna obejmować większość dostępnych opcji.
Shafik Yaghmour,
@ K-ballo nie ma możliwości zrobienia tego w C ++ na wypadek, gdybyś potrzebował wymuszonego typu argumentu .. nie ma konstrukcji takiej jak foo (int ... wartości): / Jeśli nie zależy ci na typach, to tak, szablony variadic w C ++ 11 działa świetnie
graywolf

Odpowiedzi:

152

Prawdopodobnie nie powinieneś i prawdopodobnie możesz robić to, co chcesz robić w bezpieczniejszy i prostszy sposób. Technicznie, aby użyć zmiennej liczby argumentów w C, dołączamy stdarg.h. Od tego otrzymasz va_listtyp, a także trzy działające na nim funkcje o nazwie va_start(), va_arg()i va_end().

#include<stdarg.h>

int maxof(int n_args, ...)
{
    va_list ap;
    va_start(ap, n_args);
    int max = va_arg(ap, int);
    for(int i = 2; i <= n_args; i++) {
        int a = va_arg(ap, int);
        if(a > max) max = a;
    }
    va_end(ap);
    return max;
}

Jeśli mnie zapytasz, to jest bałagan. Wygląda źle, jest niebezpieczny i pełen technicznych szczegółów, które nie mają nic wspólnego z tym, co koncepcyjnie próbujesz osiągnąć. Zamiast tego rozważ użycie przeciążenia lub dziedziczenia / polimorfizmu, wzorca konstruktora (jak w operator<<()strumieniach) lub domyślnych argumentów itp. Wszystko to jest bezpieczniejsze: kompilator dowie się więcej o tym, co próbujesz zrobić, więc jest więcej okazji, aby zatrzymać zanim zdejmiesz nogę.

wilhelmtell
źródło
7
Przypuszczalnie nie można przekazać referencji do funkcji varargs, ponieważ kompilator nie wiedziałby, kiedy przekazać wartość, a kiedy przez referencję, a ponieważ bazowe makra C niekoniecznie wiedziałyby, co zrobić z referencjami - istnieją już ograniczenia możesz przejść do funkcji C ze zmiennymi argumentami z powodu takich rzeczy, jak reguły promocji.
Jonathan Leffler,
3
czy konieczne jest podanie przynajmniej jednego argumentu przed ...składnią?
Lazer
3
@Lazer nie jest wymaganiem językowym ani bibliotecznym, ale standardowa biblioteka nie pozwala określić długości listy. Potrzebujesz dzwoniącego, aby przekazać Ci te informacje, albo sam jakoś się zorientujesz. W przypadku printf(), na przykład, funkcja analizuje argument ciągu dla specjalnych tokenów, aby dowiedzieć się, ile dodatkowych argumentów powinien się spodziewać na liście zmiennych.
wilhelmtell
11
prawdopodobnie powinieneś użyć <cstdarg>w C ++ zamiast<stdarg.h>
newacct
11
Zmienna liczba argumentów jest świetna do debugowania lub funkcji / metod, które wypełniają pewną tablicę. Jest także świetny do wielu operacji matematycznych, takich jak maksimum, min, suma, średnia ... Nie jest bałagan, gdy się z nim nie bałagan.
Tomáš Zato - Przywróć Monikę
395

W C ++ 11 masz dwie nowe opcje, ponieważ strona referencyjna funkcji Variadic w sekcji Alternatywy mówi:

  • Szablony variadic mogą być również używane do tworzenia funkcji, które przyjmują zmienną liczbę argumentów. Często są lepszym wyborem, ponieważ nie nakładają ograniczeń na typy argumentów, nie przeprowadzają promocji integralnych i zmiennoprzecinkowych oraz są bezpieczne dla typu. (od C ++ 11)
  • Jeśli wszystkie zmienne argumenty mają wspólny typ, lista std :: initializer_list zapewnia wygodny mechanizm (choć z inną składnią) dostępu do argumentów zmiennych.

Poniżej znajduje się przykład pokazujący obie alternatywy ( zobacz na żywo ):

#include <iostream>
#include <string>
#include <initializer_list>

template <typename T>
void func(T t) 
{
    std::cout << t << std::endl ;
}

template<typename T, typename... Args>
void func(T t, Args... args) // recursive variadic function
{
    std::cout << t <<std::endl ;

    func(args...) ;
}

template <class T>
void func2( std::initializer_list<T> list )
{
    for( auto elem : list )
    {
        std::cout << elem << std::endl ;
    }
}

int main()
{
    std::string
        str1( "Hello" ),
        str2( "world" );

    func(1,2.5,'a',str1);

    func2( {10, 20, 30, 40 }) ;
    func2( {str1, str2 } ) ;
} 

Jeśli używasz gcclub clangmożemy użyć magicznej zmiennej PRETTY_FUNCTION, aby wyświetlić podpis typu funkcji, który może być pomocny w zrozumieniu, co się dzieje. Na przykład za pomocą:

std::cout << __PRETTY_FUNCTION__ << ": " << t <<std::endl ;

w poniższym przykładzie wyniki dla funkcji variadic ( zobacz na żywo ):

void func(T, Args...) [T = int, Args = <double, char, std::basic_string<char>>]: 1
void func(T, Args...) [T = double, Args = <char, std::basic_string<char>>]: 2.5
void func(T, Args...) [T = char, Args = <std::basic_string<char>>]: a
void func(T) [T = std::basic_string<char>]: Hello

W Visual Studio możesz użyć FUNCSIG .

Zaktualizuj wersję C ++ 11

Przed wersją C ++ 11 alternatywą dla std :: initializer_list byłby std :: vector lub jeden z innych standardowych kontenerów :

#include <iostream>
#include <string>
#include <vector>

template <class T>
void func1( std::vector<T> vec )
{
    for( typename std::vector<T>::iterator iter = vec.begin();  iter != vec.end(); ++iter )
    {
        std::cout << *iter << std::endl ;
    }
}

int main()
{
    int arr1[] = {10, 20, 30, 40} ;
    std::string arr2[] = { "hello", "world" } ; 
    std::vector<int> v1( arr1, arr1+4 ) ;
    std::vector<std::string> v2( arr2, arr2+2 ) ;

    func1( v1 ) ;
    func1( v2 ) ;
}

a alternatywą dla szablonów variadic byłyby funkcje variadic, chociaż nie są one bezpieczne pod względem typu i generalnie podatne na błędy i mogą być niebezpieczne w użyciu, ale jedyną potencjalną alternatywą byłoby użycie domyślnych argumentów , chociaż ma to ograniczone zastosowanie. Poniższy przykład to zmodyfikowana wersja przykładowego kodu w powiązanym odnośniku:

#include <iostream>
#include <string>
#include <cstdarg>

void simple_printf(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);

    while (*fmt != '\0') {
        if (*fmt == 'd') {
            int i = va_arg(args, int);
            std::cout << i << '\n';
        } else if (*fmt == 's') {
            char * s = va_arg(args, char*);
            std::cout << s << '\n';
        }
        ++fmt;
    }

    va_end(args);
}


int main()
{
    std::string
        str1( "Hello" ),
        str2( "world" );

    simple_printf("dddd", 10, 20, 30, 40 );
    simple_printf("ss", str1.c_str(), str2.c_str() ); 

    return 0 ;
} 

Korzystanie z funkcji variadic wiąże się również z ograniczeniami w argumentach, które można przekazać, co jest szczegółowo opisane w projekcie standardu C ++ w sekcji 5.2.2 Wywołanie funkcji w paragrafie 7 :

Gdy nie ma parametru dla danego argumentu, argument jest przekazywany w taki sposób, że funkcja odbierająca może uzyskać wartość argumentu przez wywołanie va_arg (18.7). Standardowe konwersje lvalue-to-rvalue (4.1), array-to-pointer (4.2) i function-to-pointer (4.3) są wykonywane na wyrażeniu argumentu. Po tych konwersjach, jeśli argument nie ma arytmetyki, wyliczenia, wskaźnika, wskaźnika do elementu lub typu klasy, program jest źle sformułowany. Jeśli argument ma typ klasy innej niż POD (klauzula 9), zachowanie jest niezdefiniowane. [...]

Shafik Yaghmour
źródło
Czy Twoja typenamevs classużytkowania powyżej zamierzone? Jeśli tak, proszę wyjaśnić.
kevinarpe
1
@kevinarpe nie jest zamierzone, to jednak niczego nie zmienia.
Shafik Yaghmour,
Twój pierwszy link prawdopodobnie powinien być prawdopodobnie en.cppreference.com/w/cpp/language/variadic_arguments .
Aleksiej Romanow
czy jest możliwe, aby funkcja przyjmowała initializer_listrekurencję?
idclev 463035818,
33

Rozwiązanie C ++ 17: pełne bezpieczeństwo typu + ładna składnia wywoływania

Od czasu wprowadzenia szablonów variadic w C ++ 11 i wyrażeń fold w C ++ 17 możliwe jest zdefiniowanie funkcji szablonu, która w miejscu wywołującym jest wywoływana tak, jakby była funkcją varidic, ale z zaletami :

  • bądź zdecydowanie bezpieczny dla typu;
  • praca bez informacji o liczbie argumentów w czasie wykonywania lub bez użycia argumentu „stop”.

Oto przykład mieszanych typów argumentów

template<class... Args>
void print(Args... args)
{
    (std::cout << ... << args) << "\n";
}
print(1, ':', " Hello", ',', " ", "World!");

I kolejne z wymuszonym dopasowaniem typu dla wszystkich argumentów:

#include <type_traits> // enable_if, conjuction

template<class Head, class... Tail>
using are_same = std::conjunction<std::is_same<Head, Tail>...>;

template<class Head, class... Tail, class = std::enable_if_t<are_same<Head, Tail...>::value, void>>
void print_same_type(Head head, Tail... tail)
{
    std::cout << head;
    (std::cout << ... << tail) << "\n";
}
print_same_type("2: ", "Hello, ", "World!");   // OK
print_same_type(3, ": ", "Hello, ", "World!"); // no matching function for call to 'print_same_type(int, const char [3], const char [8], const char [7])'
                                               // print_same_type(3, ": ", "Hello, ", "World!");
                                                                                              ^

Więcej informacji:

  1. Szablony Variadic, znane również jako paczka parametrów Paczka parametrów (od C ++ 11) - cppreference.com .
  2. Wyrażenia foldów wyrażenie fold (od C ++ 17) - cppreference.com .
  3. Zobacz pełną demonstrację programu na coliru.
YSC
źródło
13
pewnego dnia mam nadzieję, że template<class Head, class... Tail, class = std::enable_if_t<are_same<Head, Tail...>::value, void>>
umiem
1
@Eladian czyta to jako „Ta funkcja jest włączona tylko wtedy, gdy Headi Tail... są takie same ”, gdzie „ są takie same ” oznacza std::conjunction<std::is_same<Head, Tail>...>. Przeczytaj ostatnią definicję, ponieważ „ Headjest taki sam jak wszystkie Tail...”.
YSC
24

w c ++ 11 możesz wykonać:

void foo(const std::list<std::string> & myArguments) {
   //do whatever you want, with all the convenience of lists
}

foo({"arg1","arg2"});

inicjator listy FTW!

Markus Zancolò
źródło
19

W C ++ 11 istnieje sposób tworzenia szablonów zmiennych argumentów, które prowadzą do naprawdę eleganckiego i bezpiecznego typu funkcji do wykonywania zmiennych argumentów. Sam Bjarne podaje ładny przykład printf wykorzystujący szablony zmiennych argumentów w C ++ 11FAQ .

Osobiście uważam to za tak eleganckie, że nawet nie zawracałbym sobie głowy funkcją zmiennej argumentów w C ++, dopóki ten kompilator nie będzie obsługiwał szablonów zmiennych zmiennych C ++ 11.

Wszelaki
źródło
@donlan - Jeśli używasz C ++ 17, możesz użyć wyrażeń fold, aby w niektórych przypadkach uprościć sprawę (pomyśl twórczo, możesz użyć ,operatora z wyrażeniami fold). W przeciwnym razie nie sądzę.
Wszechobecny
15

Funkcje variadic w stylu C są obsługiwane w C ++.

Jednak większość bibliotek C ++ używa alternatywnego idiomu, np. Podczas gdy 'c' printffunkcja przyjmuje zmienne argumenty, c++ coutobiekt używa <<przeciążenia, które dotyczy bezpieczeństwa typu i narzędzi ADT (być może kosztem prostoty implementacji).

Będzie
źródło
Wydaje się również, że działa to tylko w przypadku funkcji takiej jak print, w której faktycznie występuje iteracja funkcji pojedynczego argumentu dla każdego argumentu. W przeciwnym razie po prostu inicjujesz listę i przekazujesz ją do końca std::initializer_lists... A to już wprowadza ogromną złożoność prostego zadania.
Christopher
13

Oprócz varargs lub przeciążenia, możesz rozważyć agregację argumentów w std :: vector lub innych kontenerach (na przykład std :: map). Coś takiego:

template <typename T> void f(std::vector<T> const&);
std::vector<int> my_args;
my_args.push_back(1);
my_args.push_back(2);
f(my_args);

W ten sposób zyskałbyś bezpieczeństwo typu i logiczne znaczenie tych różnych argumentów byłoby oczywiste.

Z pewnością takie podejście może mieć problemy z wydajnością, ale nie powinieneś się tym martwić, chyba że masz pewność, że nie możesz zapłacić ceny. Jest to swego rodzaju „Pythońskie” podejście do c ++ ...

Francesco
źródło
6
Czystszym byłoby nie wymuszanie wektorów. Zamiast tego użyj argumentu szablonu określającego kolekcję w stylu STL, a następnie iteruj go, używając metod początkowej i końcowej argumentu. W ten sposób możesz użyć std :: vector <T>, c ++ 11's std :: array <T, N>, std :: initializer_list <T>, a nawet stworzyć własną kolekcję.
Jens Åkerblom
3
@ JensÅkerblom Zgadzam się, ale jest to wybór, który należy przeanalizować pod kątem danego problemu, aby uniknąć nadmiernej inżynierii. Ponieważ jest to kwestia podpisu API, ważne jest, aby zrozumieć kompromis między maksymalną elastycznością a jasnością intencji / użyteczności / konserwacji itp.
Francesco
8

Jedynym sposobem jest użycie argumentów zmiennych w stylu C, jak opisano tutaj . Pamiętaj, że nie jest to zalecana praktyka, ponieważ nie jest bezpieczna dla maszyn i podatna na błędy.

Dave Van den Eynde
źródło
Przez podatne na błędy zakładam, że masz na myśli potencjalnie bardzo niebezpieczne? Zwłaszcza podczas pracy z niezaufanym wejściem.
Kevin Loney,
Tak, ale z powodu problemów z bezpieczeństwem typu. Pomyśl o wszystkich możliwych problemach, jakie ma zwykły printf: ciągi formatujące niepasujące do przekazanych argumentów i tak dalej. printf używa tej samej techniki, BTW.
Dave Van den Eynde
7

Nie ma standardowego sposobu na zrobienie tego bez uciekania się do varargs ( ...) w stylu C.

Istnieją oczywiście domyślne argumenty, które wyglądają jak zmienna liczba argumentów w zależności od kontekstu:

void myfunc( int i = 0, int j = 1, int k = 2 );

// other code...

myfunc();
myfunc( 2 );
myfunc( 2, 1 );
myfunc( 2, 1, 0 );

Wszystkie cztery wywołania funkcji wywołują myfunczmienną liczbę argumentów. Jeśli nie podano żadnych, używane są domyślne argumenty. Należy jednak pamiętać, że można pominąć tylko końcowe argumenty. Nie można na przykład pominąć ii podać tylko j.

Zoli
źródło
4

Możliwe, że chcesz przeciążenia lub parametrów domyślnych - zdefiniuj tę samą funkcję z parametrami domyślnymi:

void doStuff( int a, double termstator = 1.0, bool useFlag = true )
{
   // stuff
}

void doStuff( double std_termstator )
{
   // assume the user always wants '1' for the a param
   return doStuff( 1, std_termstator );
}

Umożliwi to wywołanie metody za pomocą jednego z czterech różnych wywołań:

doStuff( 1 );
doStuff( 2, 2.5 );
doStuff( 1, 1.0, false );
doStuff( 6.72 );

... lub możesz szukać konwencji wywoływania v_args z C.

Kieveli
źródło
2

Jeśli znasz zakres liczby argumentów, które zostaną podane, zawsze możesz użyć przeciążenia funkcji, np

f(int a)
    {int res=a; return res;}
f(int a, int b)
    {int res=a+b; return res;}

i tak dalej...

Dunya Degirmenci
źródło
2

Używając szablonów variadic, przykład do reprodukcji, console.logjak widać w JavaScript:

Console console;
console.log("bunch", "of", "arguments");
console.warn("or some numbers:", 1, 2, 3);
console.error("just a prank", "bro");

Nazwa pliku np . js_console.h:

#include <iostream>
#include <utility>

class Console {
protected:
    template <typename T>
    void log_argument(T t) {
        std::cout << t << " ";
    }
public:
    template <typename... Args>
    void log(Args&&... args) {
        int dummy[] = { 0, ((void) log_argument(std::forward<Args>(args)),0)... };
        cout << endl;
    }

    template <typename... Args>
    void warn(Args&&... args) {
        cout << "WARNING: ";
        int dummy[] = { 0, ((void) log_argument(std::forward<Args>(args)),0)... };
        cout << endl;
    }

    template <typename... Args>
    void error(Args&&... args) {
        cout << "ERROR: ";
        int dummy[] = { 0, ((void) log_argument(std::forward<Args>(args)),0)... };
        cout << endl;
    }
};
lama12345
źródło
1

Jak powiedzieli inni, varargs w stylu C. Ale możesz również zrobić coś podobnego z domyślnymi argumentami.

Thomas Padron-McCarthy
źródło
Czy mógłbyś opracować?
Pozew Fund Moniki w
@QPaysTaxes: Na przykład, jak to zrobić z domyślnymi argumentami, spójrz na odpowiedź Zoltana.
Thomas Padron-McCarthy
0

Jest to możliwe teraz ... przy użyciu boost any i szablonów W tym przypadku typ argumentów można mieszać

#include <boost/any.hpp>
#include <iostream>

#include <vector>
using boost::any_cast;

template <typename T, typename... Types> 
void Alert(T var1,Types... var2) 
{ 

    std::vector<boost::any> a(  {var1,var2...});

    for (int i = 0; i < a.size();i++)
    {

    if (a[i].type() == typeid(int))
    {
        std::cout << "int "  << boost::any_cast<int> (a[i]) << std::endl;
    }
    if (a[i].type() == typeid(double))
    {
        std::cout << "double "  << boost::any_cast<double> (a[i]) << std::endl;
    }
    if (a[i].type() == typeid(const char*))
    {
        std::cout << "char* " << boost::any_cast<const char*> (a[i]) <<std::endl;
    }
    // etc
    }

} 


void main()
{
    Alert("something",0,0,0.3);
}
Po szoku
źródło
0

Połącz rozwiązania C i C ++, aby uzyskać najprostszą semantycznie, wydajną i najbardziej dynamiczną opcję. Jeśli spieprzysz, spróbuj czegoś innego.

// spawn: allocate and initialize (a simple function)
template<typename T>
T * spawn(size_t n, ...){
  T * arr = new T[n];
  va_list ap;
  va_start(ap, n);
  for (size_t i = 0; i < n; i++)
    T[i] = va_arg(ap,T);
  return arr;
}

Użytkownik pisze:

auto arr = spawn<float> (3, 0.1,0.2,0.3);

Semantycznie wygląda to i działa dokładnie jak funkcja n-argumentowa. Pod maską możesz go rozpakować w taki czy inny sposób.

Krzysztof
źródło
-1

Możemy również użyć listy inicjalizującej, jeśli wszystkie argumenty są const i tego samego typu

pkumar0
źródło
-2
int fun(int n_args, ...) {
   int *p = &n_args; 
   int s = sizeof(int);
   p += s + s - 1;
   for(int i = 0; i < n_args; i++) {
     printf("A1 %d!\n", *p);
     p += 2;
   }
}

Wersja zwykła

Вениамин Раскин
źródło
1
I niezdefiniowane zachowanie, które nie będzie działać na niczym innym niż x86.
SS Anne