Змінна кількість аргументів у C ++?


293

Як я можу написати функцію, яка приймає змінну кількість аргументів? Чи можливо це, як?


49
У цей час із C ++ 11 відповіді на це питання сильно відрізнялися
K-бал

1
@ K-Балло я додав C ++ 11 прикладів, а так як останнім часом питання задав цей же недавно і я відчував це потребує, для того , щоб виправдати його закриття stackoverflow.com/questions/16337459 / ...
Шафік Ягмур

1
До моєї відповіді було додано попередньо C ++ 11 варіантів, тому тепер вона повинна охоплювати більшість доступних варіантів.
Шафік Ягмур

@ K-ball немає ніякого способу зробити це на C ++ у випадку, якщо вам потрібен вимушений тип аргументу .. немає конструкції, як foo (int ... значень): / Якщо вам не важливо типів, то так, варіативні шаблони в C ++ 11 чудово працює
сірий вовк

Відповіді:


152

Напевно, ви не повинні, і ви, ймовірно, можете робити те, що хочете зробити, більш безпечним і простим способом. Технічно для використання змінної кількості аргументів в C ви включаєте stdarg.h. З цього ви отримаєте va_listтип, а також три функції, що працюють на ньому va_start(), va_arg()і 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;
}

Якщо ви запитаєте мене, це безлад. Це виглядає погано, небезпечно, і воно повне технічних деталей, які не мають нічого спільного з тим, що ви концептуально намагаєтесь досягти. Натомість, подумайте про використання перевантаження або успадкування / поліморфізму, структури побудови (як у operator<<()потоках) або аргументів за замовчуванням тощо. Це все безпечніше: компілятор дізнається більше про те, що ви намагаєтесь зробити, щоб було більше випадків, коли він може зупинитися ти перед тим, як збити ногу.


7
Імовірно, ви не можете передавати посилання на функцію varargs, тому що компілятор не знає, коли потрібно передати значення і коли посилання, і тому, що макроси C, що лежать в основі, не обов'язково знають, що робити з посиланнями - вже є обмеження щодо того, що ви можете перейти до функції C зі змінними аргументами через такі речі, як правила просування.
Джонатан Леффлер

3
чи потрібно навести принаймні один аргумент перед ...синтаксисом?
Лазер

3
@Lazer - це не вимога мови чи бібліотеки, але стандартна бібліотека не дає вам дати вказати довжину списку. Вам потрібен той, хто телефонує, щоб надати вам цю інформацію або як-небудь зрозуміти самостійно. Наприклад, у випадку printf(), наприклад, функція аналізує аргумент рядка для спеціальних лексем, щоб визначити, скільки зайвих аргументів слід очікувати у списку змінних аргументів.
wilhelmtell

11
ви, ймовірно, повинні використовувати <cstdarg>C ++ замість<stdarg.h>
newacct

11
Змінна кількість аргументів чудово підходить для налагодження або для функцій / методів, які заповнюють деякий масив. Крім того, це чудово підходить для багатьох математичних операцій, таких як max, min, sum, povpre ... Це не безлад, коли з ним не возитися.
Томаш Зато - Відновіть Моніку

395

У C ++ 11 у вас є два нові параметри, оскільки в довідковій сторінці Variadic функцій у розділі Альтернативи зазначено:

  • Варіантні шаблони можуть також використовуватися для створення функцій, що приймають змінну кількість аргументів. Вони часто є кращим вибором, оскільки вони не накладають обмежень на типи аргументів, не виконують інтегральних та рекламних операцій із плаваючою комою та є безпечними для типу. (оскільки C ++ 11)
  • Якщо всі аргументи змінної мають спільний тип, std :: inicijalizer_list забезпечує зручний механізм (хоча і з іншим синтаксисом) для доступу до змінних аргументів.

Нижче наведено приклад, що показує обидві альтернативи ( дивіться це в прямому ефірі ):

#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 } ) ;
} 

Якщо ви використовуєте gccабо clangми можемо використовувати магічну змінну PRETTY_FUNCTION для відображення підпису типу функції, яка може бути корисною для розуміння того, що відбувається. Наприклад, використовуючи:

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

призведе до int наступного для варіативних функцій у прикладі ( дивіться його в прямому ефірі ):

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

У Visual Studio ви можете використовувати FUNCSIG .

Оновіть попередньо C ++ 11

Попередньо C ++ 11 альтернативою std :: inicijalizer_list буде std :: вектор або один з інших стандартних контейнерів :

#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 ) ;
}

і альтернативою для варіантів шаблонів будуть варіативні функції, хоча вони не є безпечними для типу і загалом схильні до помилок і можуть бути небезпечними для використання, але єдиною іншою можливою альтернативою було б використання аргументів за замовчуванням , хоча це обмежене використання. Приклад нижче - це модифікована версія зразкового коду у зв'язаній посиланнях:

#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 ;
} 

Використання різноманітних функцій також має обмеження в аргументах, які ви можете передати, що детально описано в проекті стандарту C ++ у розділі Пункт 7 5.2.2 виклику функцій :

Коли для даного аргументу немає параметра, аргумент передається таким чином, що функція прийому може отримати значення аргументу шляхом виклику va_arg (18.7). Стандартне перетворення lvalue-to-rvalue (4.1), array-to-pointer (4.2) та function-to-pointer (4.3) виконуються на виразі аргументу. Після цих перетворень, якщо аргумент не має арифметики, перерахування, вказівника, вказівника на член або тип класу, програма неправильно формується. Якщо аргумент має тип класу без POD (пункт 9), поведінка не визначена. [...]


Є чи ваш typenameпроти classвикористання вище навмисним? Якщо так, то поясніть, будь ласка.
kevinarpe

1
@kevinarpe не навмисне, він нічого не змінює.
Шафік Ягмур

Ваше перше посилання, мабуть, має бути, ймовірно, на en.cppreference.com/w/cpp/language/variadic_argument .
Олексій Романов

чи можна зробити функцію, що приймає initializer_listрекурсивну форму ?
idclev 463035818

33

Рішення C ++ 17: безпека повного типу + синтаксис приємного виклику

Оскільки введення варіабельних шаблонів у C ++ 11 та складання виразів на C ++ 17, можна визначити шаблон-функцію, яка на сайті виклику може викликатись так, ніби це була варидична функція, але з перевагами для :

  • бути сильно безпечним;
  • працювати без інформації про час виконання кількості аргументів або без використання аргументу "стоп".

Ось приклад змішаних типів аргументів

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

І ще одна з примусовою відповідністю типу для всіх аргументів:

#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!");
                                                                                              ^

Більше інформації:

  1. Варіантні шаблони, також відомі як пакет параметрів Пакет параметрів (починаючи з C ++ 11) - cppreference.com .
  2. Складені вирази складають вираз (оскільки C ++ 17) - cppreference.com .
  3. Дивіться повну демонстрацію програми про coliru.

13
одного дня погана надія, яку я можу прочитатиtemplate<class Head, class... Tail, class = std::enable_if_t<are_same<Head, Tail...>::value, void>>
Еладіан

1
@Eladian прочитав це як "Ця річ ввімкнена лише у випадку, якщо HeadіTail... ті ж », де « одні й ті ж » означає std::conjunction<std::is_same<Head, Tail>...>. Прочитайте цей останній визначення як " Headтакий самий, як і всі Tail...".
YSC

24

в c ++ 11 ви можете:

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

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

список ініціалізаторів FTW!


19

У C ++ 11 є спосіб робити шаблони змінних аргументів, які призводять до дійсно елегантного та безпечного способу мати функції змінних аргументів. Сам Б'ярн дає хороший приклад printf, використовуючи шаблони змінних аргументів у C ++ 11FAQ .

Особисто я вважаю це настільки елегантним, що я б навіть не переймався функцією змінного аргументу в C ++, поки цей компілятор не підтримає шаблони змінних аргументів C ++ 11.


@donlan - Якщо ви використовуєте C ++ 17, ви можете використовувати вирази складок, щоб в деяких випадках зробити речі набагато простішими (подумайте творчо тут, ви можете використовувати ,оператора зі складаннями виразів). Інакше я не думаю.
всезначний

15

У C ++ підтримуються різноманітні функції стилю C.

Однак більшість бібліотек C ++ використовують альтернативну ідіому, наприклад, тоді як 'c' printfфункція бере змінні аргументи, c++ coutоб'єкт використовує <<перевантаження, яка стосується типу безпеки та ADT (можливо, ціною простоти реалізації).


Крім того, це, здається, працює лише у випадку такої функції, як друк, де ви фактично маєте ітерацію однієї функції аргументу для кожного аргументу. В іншому випадку ви просто ініціалізуєте список і передаєте список для кінця далі std::initializer_lists... І це вже вводить величезну складність у простому завданні.
Крістофер

13

Крім varargs або перевантаження, ви можете розглянути можливість об'єднання своїх аргументів у std :: vector або інших контейнерах (наприклад, std :: map). Щось на зразок цього:

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);

Таким чином, ви отримаєте безпеку типу, і логічне значення цих різних аргументів буде очевидним.

Безумовно, такий підхід може мати проблеми з роботою, але ви не повинні турбуватися про них, якщо ви не впевнені, що не можете сплатити ціну. Це свого роду підхід "піфонічний" до c ++ ...


6
Чистішим було б не застосовувати векторів. Замість цього використовуйте аргумент шаблону із зазначенням колекції, стилізованої до STL, а потім повторіть його, використовуючи методи початку та кінця аргументу. Таким чином ви можете використовувати std :: vector <T>, c ++ 11-й std :: масив <T, N>, std :: inicijalizer_list <T> або навіть зробити власну колекцію.
Єнс Екерблом

3
@ JensÅkerblom Я погоджуюсь, але це такий вибір, який слід проаналізувати для розглянутої проблеми, щоб уникнути зайвої інженерії. Оскільки це питання підпису API, важливо зрозуміти компроміс між максимальною гнучкістю та чіткістю наміру / зручності використання / ремонтопридатності тощо
Francesco

8

Єдиний спосіб - через використання змінних аргументів стилю C, як описано тут . Зауважте, що це не рекомендована практика, оскільки це не безпечно і не схильне до помилок.


Під схильністю до помилок я припускаю, що ви маєте на увазі потенційно дуже небезпечну? Особливо при роботі з ненадійним входом.
Кевін Лоні

Так, але через проблеми безпеки типу. Подумайте про всі можливі проблеми, з якими має звичайний printf: рядки формату, що не відповідають переданим аргументам, тощо. printf використовує ту саму техніку, BTW.
Дейв Ван ден Ейнде

7

Не існує стандартного способу C ++, щоб це зробити, не вдаючись до varargs у стилі C ( ...).

Звичайно, є аргументи за замовчуванням, які "виглядають" як змінна кількість аргументів залежно від контексту:

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

// other code...

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

Усі чотири функціональні дзвінки викликають myfuncрізну кількість аргументів. Якщо нічого не вказано, використовуються аргументи за замовчуванням. Зауважте, що ви можете опускати лише аргументи, що тривають. Немає способу, наприклад, опустити iі дати лише j.


4

Можливо, ви хочете перевантажувати параметри або параметри за замовчуванням - визначте ту саму функцію з параметрами за замовчуванням:

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 );
}

Це дозволить викликати метод одним із чотирьох різних викликів:

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

... або ви можете шукати конвенції про виклики v_args від C.


2

Якщо ви знаєте діапазон кількості аргументів, які будуть надані, ви завжди можете використовувати якусь функцію перевантаження, наприклад

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

і так далі...


2

Використовуючи різні шаблони, наприклад, для відтворення, console.logяк це видно в JavaScript:

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

Назва файлу, наприклад 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;
    }
};

1

Як говорили інші, вараги у стилі С. Але ви також можете зробити щось подібне з аргументами за замовчуванням.


Не могли б ви детальніше?
Фонд позову Моніки

@QPaysTaxes: Для прикладу того, як це зробити за допомогою аргументів за замовчуванням, подивіться на відповідь Золтана.
Томас Падрон-Маккарті

0

Можна зараз ... за допомогою boost any і шаблонів У цьому випадку тип аргументів можна змішувати

#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);
}

0

Поєднайте рішення C і C ++ для семантично найпростішого, найефективнішого та найбільш динамічного варіанту. Якщо ви накрутитеся, спробуйте щось інше.

// 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;
}

Користувач пише:

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

Семантично це виглядає і виглядає точно як функція n-аргументу. Під кришкою ви можете розпакувати його так чи інакше.


-1

Ми також можемо використовувати Initilizer_list, якщо всі аргументи const і одного типу


-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;
   }
}

Простий варіант


1
І невизначена поведінка, яка не буде працювати ні на чому, крім x86.
СС Енн
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.