Все началось с ошибки, как это обычно бывает. Это был мой первый опыт работы с Java Native Interface, я писал оболочку C ++ для функции Java для создания различных объектов Java. Эта функция - CallVoidMethod - является переменной, и это означает, что ее аргументы - указатель на среду JNI, указатель на новый класс объекта, идентификатор метода для вызова (конструктор в данном конкретном случае) и произвольное количество любых других аргументов. И это разумно, поскольку как пользователь JNI я хочу вызывать любой метод любого типа, и этот любой метод может принимать любое количество аргументов любого типа.

Так что я тоже делал свою обертку вариативной. Чтобы передать произвольное количество аргументов в CallVoidMethod, я использовал va_list, поскольку у меня там не было вариантов. Да, именно так я и поступил - перешел с va_list на CallVoidMethod. И разбил JVM по тривиальной ошибке сегментации.

В течение следующих 2 часов я перепробовал несколько версий на JVM, с 8 по 11. Не очень умное решение, но: во-первых, это был мой первый опыт работы с JVM, и я больше доверял StackOverflow, чем себе, а во-вторых - кто-то из StackOverflow предложил использовать OracleJDK вместо OpenJDK и версию 10 вместо версии 8. Только после нескольких сбоев со всеми этими JVM я наконец заметил, что кроме вариативного CallVoidMethod было CallVoidMethodV , который также принимает любое количество аргументов, но через va_list в качестве последнего аргумента.

Что мне больше всего не понравилось, так это то, что я сначала упустил разницу между многоточием (или тремя точками…) и va_list. И когда я это заметил, я не мог себе объяснить, в чем именно разница. Итак, пора углубиться и в многоточие, и в va_list, и (поскольку мы говорим о C ++) в вариативные шаблоны.

Что стандарт говорит о многоточии и va_list

Когда речь идет о заголовке <stdarg.h>, стандарт C ++ фактически говорит только о своих новых ограничениях по сравнению со стандартом C. Мы поговорим об этих новых ограничениях позже, а сейчас я кратко расскажу о стандарте C.

  • Можно объявить функцию с переменным количеством аргументов, то есть количество аргументов функции может быть больше количества ее параметров. Для этого список параметров функций должен заканчиваться многоточием и содержать хотя бы один обычный параметр [C11 6.9.1 / 8]:
void foo(int parm1, int parm2, ...);
  • Никакая информация о количестве или типах безымянных аргументов не передается в функцию [C11 6.7.6.3/9]. Функция буквально не знает, каковы ее аргументы, следующие за последним, или крайним правым (parm2 в примере выше).
  • Чтобы получить доступ к этим безымянным аргументам, вы должны включить заголовок <stdarg.h> и использовать тип va_list и 4 (3 перед C11) макроса: va_start, va_arg, va_end и va_copy (начиная с C11) [C11 7.16]. Пример:
int add(int count, ...)
{
    int result = 0;
    va_list args;
    va_start(args, count);
    for (int i = 0; i < count; ++i)
    {
        result += va_arg(args, int);
    }
    va_end(args);
    return result;
}

Обратите внимание, что функция ничего не знает о количестве своих фактических аргументов. Таким образом, вызывающий должен каким-то образом предоставить эту информацию. В данном конкретном случае это делается с помощью единственного именованного аргумента. Другой популярный вариант - передать трейлер NULL или 0 в качестве последнего аргумента (как в функции execl ).

  • Последний поименованный параметр не может иметь register класс хранения, не может быть функцией или массивом. Нарушение этого правила приводит к неопределенному поведению [C11 7.16.1.4/4].
  • Более того, крайний правый аргумент и все безымянные аргументы являются объектами для продвижения аргумента по умолчанию. Таким образом, если тип реального аргумента - char, short (со знаком или без него) или float, соответствующий параметр должен рассматриваться как int, int ( со знаком или без него. ) или double. В противном случае - неопределенное поведение [C11 7.16.1.1./2].
  • Все, что известно о типе va_list , это то, что он объявлен в заголовке <stdarg.h> и является полным (т.е. известен его размер) [C11 7.16 / 3].

Разве этот механизм не странный или избыточный?

В языке C действительно не так много типов. Почему va_list упоминается в Стандарте, но ничего не известно о его внутренней структуре?

Зачем нам нужно многоточие, если мы можем передавать через это va_list любое количество аргументов? Что ж, в настоящее время ответом может быть «синтаксический сахар», но я почти уверен, что 40 лет назад никто не думал о синтаксическом сахаре.

Филип Джеймс Плоджер в своей книге Стандартная библиотека C (1992) пишет, что в первые дни C был языком только для компьютеров PDP-11. И перебирать параметры функций было так же просто, как использовать простую арифметику с указателями. Проблема возникла по мере того, как C становился все более популярным и использовался на машинах с другими типами архитектуры. В первом издании Языка программирования C Брайана Кернигана и Денниса Ричи (1978) прямо написано:

Между прочим, не существует полностью удовлетворительного способа написать переносимую функцию, которая принимает переменное количество аргументов, потому что нет переносимого способа для вызываемой функции определить, сколько аргументов было фактически передано ей в данном вызове. … printf, самая распространенная функция C с переменным числом аргументов,… также непереносима и должна быть изменена для разных сред.

Итак, вы можете найти printf описание в этой книге, но еще нет макросов vprintf или va_*. Все это появилось во втором издании Языка программирования C (1988) благодаря комитету X3J11, который создал первый стандарт C (C89 или ANSI C). Комитет добавил к Стандарту заголовок <stdarg.h> на основе уже существующего, но нестандартного <varargs.h> (который был создан Эндрю Кенигом, чтобы сделать UNIX более переносимым). Было решено оставить va_* в качестве макросов, чтобы все основные компиляторы C могли легко принять новый стандарт.

Теперь, благодаря макросам C89 и va_*, разработчики, наконец, получили возможность создавать переносимые вариативные функции. И хотя Стандарт по-прежнему ничего не говорит о реализации этих макросов (и типа va_list), по крайней мере, я вижу причины для этого.

Чтобы удовлетворить свое любопытство, вы можете легко найти несколько примеров <stdarg.h> реализации. Например, в «Стандартной библиотеке C» есть библиотека Borland Turbo C ++:

#ifndef _STADARG
#define _STADARG

#define _AUPBND 1
#define _ADNBND 1

typedef char* va_list

#define va_arg(ap, T) \
 (*(T*)(((ap) += _Bnd(T, _AUPBND)) - _Bnd(T, _ADNBND)))

#define va_end(ap) \
 (void)0

#define va_start(ap, A) \
 (void)((ap) = (char*)&(A) + _Bnd(A, _AUPBND))

#define _Bnd(X, bnd) \
 (sizeof(X) + (bnd) & ~(bnd))
#endif

Гораздо более актуальный System V ABI для AMD64 использует следующий тип как va_list:

typedef struct
{
    unsigned int gp_offset;
    unsigned int fp_offset;
    void *overflow_arg_area;
    void *reg_save_area;
} va_list[1];

В общем, вы должны рассматривать тип va_* и макросы как стандартный интерфейс для перебора всех аргументов вариативной функции. По некоторым историческим причинам его реализация зависит от компилятора и целевой платформы и архитектуры. Обратите внимание, что многоточие (и функции с переменными числами в целом) были доступны в C задолго до va_list. И весь заголовок <stdarg.h> был добавлен в язык не для замены многоточия, а для обеспечения переносимого способа создания вариативных функций.

Поскольку C ++ обеспечивает обратную совместимость с C, все вышеперечисленное относится и к C ++. И это еще не все.

Вариативные функции C ++

Комитет по разработке языка программирования C ++ также известен как рабочая группа 21 (WG21). Работа над стандартом C ++ началась в 1989 году, как только был выпущен стандарт C. WG21 приняла C89 в качестве базового документа и шаг за шагом превратила его в новый документ, описывающий C ++. В 1995 году Джон Микко применил предложение N0695 к стандарту C ++, в котором он предложил несколько изменений для va_* ограничений макросов:

  • Поскольку адрес регистровой переменной может быть взят в C ++ (в отличие от C), крайний правый аргумент вариативных функций должен иметь класс хранения register .
  • Поскольку ссылочные типы в C ++ нарушают неустановленное правило вариативных функций C, согласно которому размер параметра должен соответствовать его объявленному типу, крайний правый параметр не должен быть ссылочного типа. В противном случае это должно быть неопределенное поведение.
  • Поскольку C ++ не определяет «продвижение аргументов по умолчанию», формулировка

Если параметр parmN объявлен с… типом, несовместимым с типом, который возникает после применения продвижений аргументов по умолчанию, поведение не определено.

следует заменить на

Если параметр parmN объявлен с… типом, несовместимым с типом, который возникает при передаче аргумента, для которого нет параметра, поведение не определено.

Ну, первое, что я хотел бы отметить, это то, что C ++ действительно определяет «продвижение аргументов по умолчанию» [C ++ 17 8.2.2 / 9]. И второй - формулировка, предложенная Micco, гораздо менее очевидна, чем старая. Его вряд ли можно понять, не прочитав само предложение N0696.

Тем не менее, все 3 изменения были внесены в Стандарт [C ++ 98 18.7 / 3]. Еще одно отличие состоит в том, что в C ++ вариативная функция может иметь ноль обычных аргументов, также называемых именованными аргументами (в таком случае нет доступа ни к одному из аргументов, но давайте поговорим об этом позже). Также безымянные аргументы в C ++ 98 могут быть указателями на члены класса и типы POD.

Стандарт C ++ 03 не принес ничего нового для вариативных функций. Поскольку в C ++ 11 безымянные аргументы типа std::nullptr_t повышаются до void*, а аргументы нетривиальных типов «условно поддерживаются семантикой, определяемой реализацией» [C ++ 11 5.2.2 / 7], т.е. аргументы теперь зависят от доброй воли компилятора. Поскольку функции и массивы C ++ 14 разрешены в качестве крайних правых аргументов в функциях с переменным числом аргументов [C ++ 14 18.10 / 3]. А так как расширения пакетов C ++ 17 и объекты, захваченные лямбда-выражением, не допускаются в качестве последнего названного аргумента [C ++ 17 21.10.1 / 1].

Несомненно, C ++ добавил некоторые подводные камни разработчикам вариативных функций. Одной неуказанной поддержки нетривиальных типов более чем достаточно. В следующем разделе я постараюсь перечислить все неочевидные особенности и подводные камни вариативных функций C ++ и проиллюстрировать их некоторыми примерами кода.

Как использовать вариативные функции легко, но неправильно

1. Не объявляйте крайний правый аргумент с продвигаемым типом - char, signed char, unsigned char, signed short, unsigned short или float. Согласно Стандарту это приведет к неопределенному поведению.

void foo(float n, ...)
{
    va_list va;
    va_start(va, n);
    std::cout << va_arg(va, int) << std::endl;
    va_end(va);
}

Среди всех доступных мне сейчас компиляторов (gcc, clang, MSVC) только clang выдал предупреждение:

./test.cpp:7:18: warning: passing an object that undergoes default argument promotion to 'va_start' has undefined behavior [-Wvarargs]
    va_start(va, n);
                 ^

Собственно, этот код, скомпилированный любым компилятором, в моем конкретном случае был выполнен правильно. Но рассчитывать на это не стоит! Правильный способ - объявить n как double:

void foo(double n, ...)
{
    va_list va;
    va_start(va, n);
    std::cout << va_arg(va, int) << std::endl;
    va_end(va);
}

2. Не объявляйте крайний правый аргумент ссылочного типа. Любой справочный тип. Результатом также будет неопределенное поведение.

void foo(int& n, ...)
{
    va_list va;
    va_start(va, n);
    std::cout << va_arg(va, int) << std::endl;
    va_end(va);
}

gcc 7.3.0 не выдал предупреждений, clang 6.0.0 сделал, но все же завершил компиляцию:

./test.cpp:7:18: warning: passing an object of reference type to 'va_start' has undefined behavior [-Wvarargs]
    va_start(va, n);
                 ^

В обоих случаях скомпилированный код выполнялся корректно (мне повезло, не рассчитывайте на это!). Но MSCV 19.15.26730 отказался компилировать этот код, обнаружив, что первый аргумент va_start был ссылкой:

1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\
msvc\14.15.26726\include\vadefs.h(151): error C2338: va_start argument must not have reference type and must not be parenthesized

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

void foo(int* n, ...)
{
    va_list va;
    va_start(va, n);
    std::cout << va_arg(va, int) << std::endl;
    va_end(va);
}

3. Не запрашивайте va_arg для продвигаемого типа - char, short или float.

#include <cstdarg>
#include <iostream>

void foo(int n, ...)
{
    va_list va;
    va_start(va, n);
    std::cout << va_arg(va, int) << std::endl;
    std::cout << va_arg(va, float) << std::endl;
    std::cout << va_arg(va, int) << std::endl;
    va_end(va);
}

int main()
{
    foo(0, 1, 2.0f, 3);
    return 0;
}

Эта ошибка более интересная. gcc выдал предупреждение и предложил использовать double вместо float, иначе программа выйдет из строя:

./test.cpp:9:15: warning: ‘float’ is promoted to ‘double’ when passed through ‘...’
    std::cout << va_arg(va, float) << std::endl;
                 ^~~~~~
./test.cpp:9:15: note: (so you should pass ‘double’ not ‘float’ to ‘va_arg’)
./test.cpp:9:15: note: if this code is reached, the program will abort

И он был бы разбит незаконной инструкцией. Анализ дампа показал, что программа получила сигнал SIGILL . Также в дампе была структура va_list! Для 32 бит это было:

va = 0xfffc6918 ""

то есть va_list это просто char*. Для 64 бит:

va = {{gp_offset = 16, fp_offset = 48, overflow_arg_area = 0x7ffef147e7e0, reg_save_area = 0x7ffef147e720}}

это именно то, что указывает на использование SystemV ABI AMD64.

Clang предупредил о неопределенном поведении, а также предложил заменить float на double:

/test.cpp:9:26: warning: second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double' [-Wvarargs]
    std::cout << va_arg(va, float) << std::endl;
                            ^~~~~

Но скомпилированный код не сломался. 32-битный вариант дал следующий результат:

1
0
1073741824

И 64-битный:

1
0
3

MSVC 19.15.26730 выдал точно такой же результат, но без предупреждений вообще, даже с флагом /Wall.

Моя первая мысль заключалась в том, что эта разница между 32 и 64 битами связана с разными преобразованиями вызовов: 32-битный ABI передает все аргументы для вызываемой функции через стек, тогда как 64-битный передает первые четыре (в Windows) или шесть (в Linux) аргументов через регистры процессора, остальные - через стек [wiki]. Но нет, если foo вызывается с 19 аргументами вместо 4, результат тот же: полный беспорядок для 32 бит и нули вместо всех float для 64 бит. Что ж, причина, очевидно, находится где-то в ABI, но дело не в способе передачи аргументов функции. И правильный код выглядит так:

void foo(int n, ...)
{
    va_list va;
    va_start(va, n);
    std::cout << va_arg(va, int) << std::endl;
    std::cout << va_arg(va, double) << std::endl;
    std::cout << va_arg(va, int) << std::endl;
    va_end(va);
}

4. Не передавайте экземпляры нетривиальных типов в качестве безымянных аргументов. Если по какой-то причине вы заботитесь о своем коде немного больше, чем «компилируйте здесь и сейчас».

#include <cstdarg>
#include <iostream>

struct Bar
{
    Bar() { std::cout << "Bar default ctor" << std::endl; }
    Bar(const Bar&) { std::cout << "Bar copy ctor" << std::endl; }
    ~Bar() { std::cout << "Bar dtor" << std::endl; }
};

struct Cafe
{
    Cafe() { std::cout << "Cafe default ctor" << std::endl; }
    Cafe(const Cafe&) { std::cout << "Cafe copy ctor" << std::endl; }
    ~Cafe() { std::cout << "Cafe dtor" << std::endl; }
};

void foo(int n, ...)
{
    va_list va;
    va_start(va, n);
    std::cout << "Before va_arg" << std::endl;
    const auto b = va_arg(va, Bar);
    va_end(va);
}

int main()
{
    Bar b;
    Cafe c;
    foo(1, b, c);
    return 0;
}

И снова clang был самым строгим компилятором. Он просто отказался компилировать этот код, поскольку второй аргумент va_arg не был типом POD, и предупредил, что программа выйдет из строя:

/test.cpp:23:31: error: second argument to 'va_arg' is of non-POD type 'Bar' [-Wnon-pod-varargs]
    const auto b = va_arg(va, Bar);
                              ^~~
./test.cpp:31:12: error: cannot pass object of non-trivial type 'Bar' through variadic function; call will abort at runtime [-Wnon-pod-varargs]
    foo(1, b, c);
           ^

И будет, если к флагам компилятора добавить -Wno-non-pod-varargs.

MSVC предупредил, что использование нетривиальных типов в этом контексте не переносимо:

1>d:\my documents\visual studio 2017\projects\test\test\main.cpp(31):
warning C4840: non-portable use of class 'Bar' as an argument to a
variadic function
1>d:\my documents\visual studio 2017\projects\test\test\main.cpp(31): note:
the constructor and destructor will not be called; a bitwise copy of
the class will be passed as the argument

Но код был скомпилирован и выполнен правильно. Результат был:

Bar default ctor
Cafe default ctor
Before va_arg
Bar copy ctor
Bar dtor
Cafe dtor
Bar dtor

Как и обещал компилятор, новая копия была создана только при вызове va_arg, поэтому аргументы передавались по значению, но конструктор не вызывался! Это не очевидно, но Стандарт позволяет это.

gcc 6.3.0 скомпилировал этот код без единого предупреждения, и результат был таким же. gcc 7.3.0 также не выдавал предупреждения, но поведение и результат были разными:

Bar default ctor
Cafe default ctor
Cafe copy ctor
Bar copy ctor
Before va_arg
Bar copy ctor
Bar dtor
Bar dtor
Cafe dtor
Cafe dtor
Bar dtor

Таким образом, эта версия gcc также передает аргументы по значению, но вызывает и конструктор, и деструктор, и - делает еще одну копию при вызове va_arg . Вы можете себе представить удовольствие от поиска этой разницы после перехода с 6-й на 7-ю версию gcc, особенно если конструктор или деструктор имеет какой-либо побочный эффект. Кстати, если вы явно передадите ссылку как безымянный аргумент:

void foo(int n, ...)
{
    va_list va;
    va_start(va, n);
    std::cout << "Before va_arg" << std::endl;
    const auto& b = va_arg(va, Bar&);
    va_end(va);
}

int main()
{
    Bar b;
    Cafe c;
    foo(1, std::ref(b), c);
    return 0;
}

ни одна из упомянутых компиляций не скомпилирует код. Правильно, Стандарт запрещает это.

Что ж, если это действительно нужно, передайте по указателю аргументы нетривиальных типов:

void foo(int n, ...)
{
    va_list va;
    va_start(va, n);
    std::cout << "Before va_arg" << std::endl;
    const auto* b = va_arg(va, Bar*);
    va_end(va);
}

int main()
{
    Bar b;
    Cafe c;
    foo(1, &b, &c);
    return 0;
}

Разрешение перегрузки и вариативные функции

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

#include <iostream>

void foo(...)
{
    std::cout << "C variadic function" << std::endl;
}

void foo(int)
{
    std::cout << "Ordinary function" << std::endl;
}

int main()
{
    foo(1);
    foo(1ul);
    foo();
    return 0;
}

$ ./test
Ordinary function
Ordinary function
C variadic function

Все хорошо и понятно, пока вам не понадобится обрабатывать вызов foo без аргументов:

#include <iostream>

void foo(...)
{
    std::cout << "C variadic function" << std::endl;
}

void foo()
{
    std::cout << "Ordinary function without arguments" << std::endl;
}

int main()
{
    foo(1);
    foo();
    return 0;
}

./test.cpp:16:9: error: call of overloaded ‘foo()’ is ambiguous
    foo();
        ^
./test.cpp:3:6: note: candidate: void foo(...)
    void foo(...)
         ^~~
./test.cpp:8:6: note: candidate: void foo()
    void foo()
         ^~~

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

Когда вам действительно нужно использовать вариативные функции?

Хорошо, вариативные функции в стиле C иногда довольно неочевидны и часто непереносимы в контексте C ++. В Интернете можно найти множество советов типа «Не создавайте и не используйте вариативные функции Си». Но в стандарте C ++ эти функции по-прежнему есть, и комитет не собирается их удалять. Для этого должна быть какая-то причина, верно? Так и есть.

1. Самый очевидный и частый случай - обратная совместимость. Здесь я имею в виду использование сторонних библиотек C (например, мою историю JNI), а также предоставление C API для некоторых реализаций C ++.

2. СФИНАЭ. Замечательно то, что в C ++ вариативные функции не могут иметь обычных параметров. Также вариативная функция является наименее подходящим кандидатом в разрешении перегрузки (если вызов функции имеет хотя бы один аргумент). Как и любую другую функцию, вариативную функцию можно только объявить, но никогда не вызвать:

template <class T>
struct HasFoo
{
private:
    template <class U, class = decltype(std::declval<U>().foo())>
    static void detect(const U&);

    static int detect(...);

public:
    static constexpr bool value = 
    std::is_same<void, decltype(detect(std::declval<T>()))>::value;
};

При использовании C ++ 14 вы можете сделать это немного более читаемым способом:

template <class T>
struct HasFoo
{
private:
    template <class U, class = decltype(std::declval<U>().foo())>
    static constexpr bool detect(const U*)
    {
        return true;
    }

    template <class U>
    static constexpr bool detect(...){
        return false;
    }

public:
    static constexpr bool value = detect<T>(nullptr);
};

Но теперь вы должны быть очень осторожны с тем, какие аргументы передаются в detect(...). Я лично предпочитаю изменить пару строк, чтобы использовать современную альтернативу старым вариативным функциям, у которой нет ни одного из их недостатков.

Вариативные шаблоны или правильный способ создания функций с произвольным количеством аргументов в современном C ++

Вариативные шаблоны были предложены Дугласом Грегором, Яакко Ярви и Гэри Пауэллом в 2004 году, за 7 лет до выпуска C ++ 11 - первого стандарта, поддерживающего эти шаблоны. Стандарт принял 3-ю редакцию их предложения N2080.

Одна из первоначальных целей изобретения вариативных шаблонов заключалась в том, чтобы дать разработчикам безопасный по типу (и переносимый!) Способ создания вариативных функций. Другой - упростить поддержку шаблонов классов с произвольным числом аргументов и сократить использование метапрограммирования препроцессора. Но пока поговорим только о вариативных функциях.

Наряду с вариативными шаблонами в C ++ [C ++ 17 17.5.3] появились три новых концепции:

  • пакет параметров шаблона - это параметр шаблона, который принимает ноль или более аргументов шаблона;
  • Пакет параметров функции - это параметр функции, который принимает ноль или более аргументов функции;
  • и расширение пакета - фактически единственное, что вы можете сделать с любым пакетом параметров.

Например:

template <class ... Args>
void foo(const std::string& format, Args ... args)
{
    printf(format.c_str(), args...);
}

Здесь class ... Args - это пакет параметров шаблона, Args ... args - это пакет параметров функции, а args... - расширение пакета параметров функции.

Полный список всех возможных контекстов для расширения пакета вы можете найти в Стандарте [C ++ 17 17.5.3 / 4]. Говоря о вариативных функциях, достаточно упомянуть следующее:

  • пакет параметров функции может быть расширен до списка аргументов другой функции:
template <class ... Args>
void bar(const std::string& format, Args ... args)
{
    foo<Args...>(format.c_str(), args...);
}
  • или в список инициализаторов:
template <class ... Args>
void foo(const std::string& format, Args ... args)
{
    const auto list = {args...};
}
  • или в список захвата лямбды:
template <class ... Args>
void foo(const std::string& format, Args ... args)
{
    auto lambda = [&format, args...] ()
    {
        printf(format.c_str(), args...);
    };
    lambda();
}
  • или в краткое выражение:
template <class ... Args>
int foo(Args ... args)
{
    return (0 + ... + args);
}

Выражения свертки были введены в C ++ 14, они могут быть унарными или двоичными, левыми или правыми. Полное описание, как всегда, можно найти в Стандарте [C ++ 17 8.1.6].

  • оба типа пакетов параметров можно раскрыть в sizeof... выражение:
template <class ... Args>
void foo(Args ... args)
{
    const auto size1 = sizeof...(Args);
    const auto size2 = sizeof...(args);
}

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

template <class ... Args>
void foo()
{
    using OneTuple  ​= std::tuple<std::tuple<Args>...>;
    using NestedTuple = std::tuple<std::tuple<Args...>>;
}

OneTuple здесь кортеж из 1 кортежей (std::tuple<std::tuple<int>, std::tuple<double>>), но NestedTuple - это кортеж, содержащий единственный кортеж (std::tuple<std::tuple<int, double>>).

Простая реализация printf с вариативными шаблонами

Как упоминалось выше, вариативные шаблоны были введены в качестве замены старых вариативных функций. Авторы этих шаблонов продемонстрировали свою мощь, представив довольно простую, но безопасную для типов реализацию printf - одной из первых вариативных функций в C:

void printf(const char* s)
{
    while (*s)
    {
        if (*s == '%' && *++s != '%')
            throw std::runtime_error(
                  "invalid format string: missing arguments");
        std::cout << *s++;
    }
}


template <typename T, typename ... Args>
void printf(const char* s, T value, Args ... args)
{
    while (*s)
    {
        if (*s == '%' && *++s != '%')
        {
            std::cout << value;
            return printf(++s, args...);
        }
        std::cout << *s++;
    }
    throw std::runtime_error("extra arguments provided to printf");
}

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

template <typename ... Args>
void printf(const std::string& fmt, const Args& ... args)
{
    size_t fmtIndex = 0;
    size_t placeHolders = 0;
    auto printFmt = [&fmt, &fmtIndex, &placeHolders]()
    {
        for (; fmtIndex < fmt.size(); ++fmtIndex)
        {
            if (fmt[fmtIndex] != '%')
                std::cout << fmt[fmtIndex];
            else if (++fmtIndex < fmt.size())
            {
                if (fmt[fmtIndex] == '%')
                    std::cout << '%';
                else
                {
                    ++fmtIndex;
                    ++placeHolders;
                    break;
                }
            }
        }
    };

    ((printFmt(), std::cout << args), ..., (printFmt()));

    if (placeHolders < sizeof...(args))
        throw std::runtime_error(
              "extra arguments provided to printf");
    if (placeHolders > sizeof...(args))
        throw std::runtime_error(
              "invalid format string: missing arguments");  
}

Разрешение перегрузки и вариативные функции шаблона

При разрешении перегрузки такие вариативные функции имеют наименьший приоритет - как шаблонные, так и наименее специализированные. И нет никаких неожиданных проблем при вызове такой функции без аргументов:

#include <iostream>

void foo(int)
{
    std::cout << "Ordinary function" << std::endl;
}

void foo()
{
    std::cout << "Ordinary function without arguments" << std::endl;
}

template <class T>
void foo(T)
{
    std::cout << "Template function" << std::endl;
}

template <class ... Args>
void foo(Args ...)
{
    std::cout << "Template variadic function" << std::endl;
}

int main()
{
    foo(1);
    foo();
    foo(2.0);
    foo(1, 2);
    return 0;
}

$ ./test
Ordinary function
Ordinary function without arguments
Template function
Template variadic function

Только вариативная функция в стиле C может уступить шаблонной в контексте разрешения перегрузки (но зачем вам вообще их смешивать?). Очевидное исключение - отсутствие аргументов:

#include <iostream>

void foo(...)
{
    std::cout << "C variadic function" << std::endl;
}

template <class ... Args>
void foo(Args ...)
{
    std::cout << "Template variadic function" << std::endl;
}

int main()
{
    foo(1);
    foo();
    return 0;
}

$ ./test
Template variadic function
C variadic function

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

Краткое замечание о производительности вариативных функций шаблона

В 2008 году Лоик Жоли подал свое предложение N2772 в WG21. В этом предложении он на практике доказал, что вариативные функции шаблона работают медленнее, чем их аналоги, принимая список инициализаторов (std::initializer_list) в качестве единственного аргумента. Несмотря на то, что практический результат противоречил теоретическим ожиданиям автора, Джоли рекомендовал заменить вариативные реализации шаблонов std::min, std::max и std::minmax на варианты, основанные на списке инициализаторов.

Но уже в следующем, 2009 году, опровержение было опубликовано, поскольку в тестах Джоли (вероятно, самого Джоли) была обнаружена «серьезная ошибка». Согласно новому шаблону тестов вариативные функции работают быстрее, а иногда и значительно быстрее. Этот результат был полностью ожидаемым, поскольку список инициализаторов делает копию для каждого своего элемента, в то время как вариативная функция шаблона может выполнять некоторые вычисления во время компиляции.

Тем не менее, в C ++ 11 и более поздних версиях Стандарт std::min, std::max и std::minmax являются обычными шаблонными функциями, единственным аргументом которых является std::initializer_list.

Резюме и заключение

Итак, вариативные функции в стиле C:

  • Ничего не знаю ни о количестве, ни о типах их фактических аргументов. Разработчик должен предоставить эту информацию через некоторые параметры функции.
  • Неявно продвигайте типы по безымянным аргументам (и крайнему правому именованному аргументу). Если вы забудете об этом, вы получите неопределенное поведение.
  • Обратно совместимы с ANSI C и поэтому не поддерживают ссылочные типы.
  • Не поддерживал типы, отличные от POD, до C ++ 11. Начиная с C ++ 11 поддержка нетривиальных типов не указана. Т.е. поведение такого кода зависит от компилятора и его версии.

Единственный допустимый (в современном C ++) случай использования этих функций - интеграция с некоторым C API. Для всего остального, включая SFINAE, предпочтительнее использовать шаблонные вариативные функции. Причины:

  • Шаблонные вариативные функции знают как количество, так и типы своих аргументов.
  • Они типобезопасны, не меняют типы своих аргументов.
  • Поддерживает любой тип передачи аргументов - по значению, по указателю, по ссылке, по универсальной ссылке.
  • Как и любые другие функции C ++, эти функции не имеют ограничений по типам аргументов.
  • В пределах разрешения перегрузки всегда рассматриваются как наименее подходящие кандидаты. Единственное исключение - вариативные функции в стиле C.

Иногда вариативная функция шаблона может состоять из большего количества строк кода, чем ее аналог в стиле C, или даже требует самой себя перегруженной нешаблонной версии (для рекурсивной итерации по аргументам). Такую функцию сложнее читать и писать. Но все эти упомянутые минусы полностью перевешиваются упомянутыми плюсами.

Что ж, вывод прост: единственная причина сохранить вариативные функции в стиле C в C ++ - это обратная совместимость, но они предоставляют множество способов выстрелить себе в ногу. Поэтому в современном C ++ проекте новые такие функции разумно не писать, а использовать старые только в том случае, если другого выхода нет. С другой стороны, шаблонные вариативные функции относятся к современному C ++ и намного безопаснее. Они должны быть вашим выбором.

Первоначально опубликовано на https://www.linkedin.com.