Все началось с ошибки, как это обычно бывает. Это был мой первый опыт работы с 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.