CallVoidMethod
— вариативна, т.е. помимо указателя на среду JNI, указателя на тип создаваемого объекта и идентификатора вызываемого метода (в данном случае конструктора), она принимает произвольное число других аргументов. Что логично, т.к. эти другие аргументы передаются вызываемому методу на стороне Java, а методы могут быть разные, с разным числом аргументов любых типов.Соответственно и свою обертку я тоже сделал вариативной. Для передачи произвольного числа аргументов в
CallVoidMethod
использовал va_list
, потому что по-другому в данном случае никак. Да, так и отправил va_list
в CallVoidMethod
. И уронил JVM банальным segmentation fault.За 2 часа я успел перепробовать несколько версий JVM, от 8-ой до 11-ой, потому что: во-первых это мой первый опыт с JVM, и в этом вопросе я StackOverflow доверял больше, чем себе, а во-вторых кто-то на StackOverflow посоветовал в таком случае использовать не OpenJDK, а OracleJDK, и не 8, а 10. И лишь потом я наконец заметил, что помимо вариативной
CallVoidMethod
есть CallVoidMethodV
, которая произвольное число аргументов принимает через va_list
.Что мне больше всего не понравилось в этой истории, так это то, что я не сразу заметил разницу между эллипсисом (многоточием) и
va_list
. А заметив, не смог объяснить себе, в чем принципиальное отличие. Значит, надо разобраться и с эллипсисом, и с va_list
, и (поскольку речь все-таки о C++) с вариативными шаблонами.Что про эллипсис и va_list сказано в Стандарте
Стандарт C++ описывает только отличия своих требований от требований Стандарта С. О самих отличиях позже, а пока кратко перескажу, что говорит Стандарт С (начиная с C89).
- Можно объявить функцию, принимающую произвольное число аргументов. Т.е. аргументов у функции может быть больше, чем параметров. Для этого список ее параметров должен заканчиваться эллипсисом, но так же должен присутствовать хотя бы один фиксированный параметр [C11 6.9.1/8]:
void foo(int parm1, int parm2, ...);
- В саму функцию не передается информация о количестве и типах аргументов, соответствующих эллипсису. Т.е. после последнего именованного параметра (
parm2
в примере выше) [C11 6.7.6.3/9].
- Для доступа к этим аргументам надо использовать объявленные в заголовке
<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
, как вexecl
, или 0). - Последний именованный аргумент не может иметь класс хранения
register
, не может быть функцией или массивом. Иначе — неопределенное поведение [C11 7.16.1.4/4]. - Причем к последнему именованному аргументу и всем безымянным применяется «повышение типа аргумента по умолчанию» (default argument promotion; если есть хороший перевод этого понятия на русский, я с радостью его использую). Это значит, что если у аргумента тип
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 лет назад, я уверен, было не до сахара.Филип Джеймс Плаугер (Phillip James Plauger) в книге «Стандартная библиотека Си» (The Standard C library) — 1992 год — рассказывает, что изначально C создавался исключительно для компьютеров PDP-11. А там перебрать все аргументы функции можно было с помощью простой арифметики указателей. Проблема появилась с ростом популярности C и переноса компилятора на другие архитектуры. В первом издании «Языка программирования Си» (The C Programming Language) Брайана Кернигана (Brian Kernighan) и Денниса Ритчи (Dennis Ritchie) — 1978 год — прямо сказано:
Кстати, не существует приемлемого способа написать переносимую функцию от произвольного числа аргументов, т.к. нет переносимого способа для вызванной функции узнать, сколько же аргументов ей передано при вызове. … printf
, наиболее типичная функция языка C от произвольного числа аргументов, … не является переносимой и должна быть реализована для каждой системы.
В этой книге описывается printf
, но еще нет vprintf
, и не упоминаются тип и макросы va_*
. Они появляются во втором издании «Языка программирования Си» (1988 год), и это — заслуга комитета по разработке первого Стандарта Си (C89, он же ANSI C). Комитет добавил в Стандарт заголовок <stdarg.h>
, взяв за основу <varargs.h>
, созданный Эндрю Кёнигом (Andrew Koenig) с целью повысить переносимость ОС UNIX. Макросы va_*
было решено оставить макросами, чтобы существующим компиляторам было проще поддержать новый Стандарт.Теперь, с появлением С89 и семейства
va_*
, стало возможным создавать переносимые вариативные функции. И хотя внутреннее устройство этого семейства по-прежнему никак не описывается, и никаких требований к нему не предъявляется, но уже понятно, почему.Из чистого любопытства можно найти примеры реализации
<stdarg.h>
. Например, в той же «Стандартной библиотеке Си» приводится пример для 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
Гораздо более новый SystemV 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>
). И va_list
создавался не для замены эллипсиса, а для возможности разработчикам писать свои переносимые вариативные функции.С++ во многом сохраняет обратную совместимость с C, поэтому все вышесказанное относится и к нему. Но есть и свои особенности.
Вариативные функции в C++
Разработкой Стандарта C++ занималась и занимается рабочая группа WG21. За основу еще в 1989 году был взял только что созданный Стандарт С89, который постепенно менялся, чтобы описывать собственно C++. В 1995 году поступило предложение N0695 от Джона Микко (John Micco), в котором автор предлагал изменить ограничения для макросов
va_*
:- Т.к. C++ в отличие от С позволяет получать адрес
register
переменных, то последний именованный аргумент вариативной функции может иметь этот класс хранения.
- Т.к. появившиеся в C++ ссылки нарушают негласное правило вариативных функций С — размер параметра должен совпадать с размером его объявленного типа — то последний именованный аргумент не может быть ссылкой. Иначе — неопределенное поведение.
- Т.к. в C++ нет понятия «повышения типа аргумента по умолчанию», то фразу
If the parameter
надо заменить наparmN
is declared with… a type that is not compatible with the type that results after application of the default argument promotions, the behavior is undefinedIf the parameter
parmN
is declared with… a type that is not compatible with the type that results when passing an argument for which there is no parameter, the behavior is undefined
Тем не менее, все 3 изменения были приняты [C++98 18.7/3]. Еще в C++ исчезло требование вариативной функции иметь хотя бы один именованный параметр (в таком случае нельзя получить доступ и к остальным, но об этом позже), и список допустимых типов неименованных аргументов дополнился указателями на члены класса и POD типами.
Стандарт C++03 не принес вариативным функциям никаких изменений. С++11 стал конвертировать неименованный аргумент типа
std::nullptr_t
в void*
и разрешил компиляторам на свое усмотрение поддерживать типы с нетривиальными конструкторами и деструкторами [C++11 5.2.2/7]. C++14 разрешил использовать в качестве последнего именованного параметра функции и массивы [C++14 18.10/3], а C++17 запретил — раскрытия пакета параметров (pack expansion) и захваченные лямбдой переменные [C++17 21.10.1/1].В итоге C++ добавил вариативным функциям своих подводных камней. Одна только неуточняемая (unspecified) поддержка типов с нетривиальными конструкторами/деструкторами чего стоит. Ниже я постараюсь свести все неочевидные особенности вариативных функций в один список и дополнить его конкретными примерами.
Как легко и неправильно использовать вариативные функции
- Неправильно объявлять последний именованный аргумент с повышаемым типом, т.е.
char
,signed char
,unsigned char
,singed 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 выдал предупреждение.
Предупреждение от 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); ^
И хотя во всех случаях скомпилированный код вел себя корректно, рассчитывать на это не стоит.
Правильно будет такvoid foo(double n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); }
- Неправильно объявлять последний именованный аргумент ссылкой. Любой ссылкой. Стандарт и в этом случае обещает неопределенное поведение.
Неправильный код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 скомпилировал этот код без единого замечания. сlang 6.0.0 выдал предупреждение, но все-таки компиляцию выполнил.
Предупреждение от clang./test.cpp:7:18: warning: passing an object of reference type to 'va_start' has undefined behavior [-Wvarargs] va_start(va, n); ^
В обоих случаях программа отработала корректно (повезло, нельзя на это полагаться). А вот MSVC 19.15.26730 отличился — он отказался компилировать код, т.к. аргументva_start
не должен быть ссылкой.
Ошибка от MSVCc:\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); }
- Неправильно запрашивать у
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
, а если этот код все-таки будет выполнен, то программа завершится с ошибкой.
Предупреждение от gcc./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
.
Предупреждение от clang./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 выдает точно такие же результаты, только без предупреждений, даже с/Wall
.
Тут можно было бы предположить, что разница между 32 и 64 битами обусловлена тем, что в первом случае ABI передает вызванной функции все аргументы через стек, а во втором первые четыре (Windows) или шесть (Linux) аргументов через регистры процессора, остальные — через стек [wiki]. Но нет, если вызыватьfoo
не с 4 аргументами, а с 19, и так же выводить их, то результат будет прежний: полное месиво в 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); }
- Неправильно передавать в качестве безымянного аргумента экземпляр класса с нетривиальным конструктором или деструктором. Если, конечно, судьба этого кода волнует вас хоть немного больше, чем «скомпилировать и запустить здесь и сейчас».
Неправильный код#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 типом, и предупреждает, что при запуске программа упадет.
Предупреждение от clang./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 предупреждает, что использование в данном случае типов с нетривиальным конструкторов непереносимо.
Предупреждение от MSVCd:\my documents\visual studio 2017\projects\test\test\main.cpp(31): warning C4840: непереносимое использование класса "Bar" в качестве аргумента в функции с переменным числом аргументов
Но код компилируется и выполняется корректно. В консоли получается следующее:
Результат запускаBar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor
Т.е. копия создается только в момент вызоваva_arg
, а аргумент, получается, передается по ссылке. Как-то не очевидно, но Стандарт разрешает.
gcc 6.3.0 компилирует без единого замечания. На выходе имеем то же самое:
Результат запускаBar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor
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
Т.е. эта версия компилятора передает аргументы по значению, а при вызове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 библиотек (мой случай с JNI), так и предоставление C API к C++ реализации.
- SFINAE. Тут очень кстати и то, что в 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++
Идея вариативных шаблонов была предложена Дугласом Грегором (Douglas Gregor), Яакко Ярви (Jaakko Jarvi) и Гэри Пауэллом (Gary Powell) еще в 2004 году, т.е. за 7 лет до принятия стандарта C++11, в котором эти вариативные шаблоны и были официально поддержаны. В Стандарт вошла третья ревизия их предложения за номером N2080.
С самого начала вариативные шаблоны создавались, чтобы у программистов была возможность создавать типобезопасные (и переносимые!) функции от произвольного числа аргументов. Другая цель — упростить поддержку шаблонов классов с переменным числом параметров, но сейчас речь идет только о вариативных функциях.
Вариативные шаблоны принесли в C++ три новых понятия [C++17 17.5.3]:
- пакет параметров шаблона (template parameter pack) — это такой параметр шаблона, вместо которого можно передать любое (включая 0) количество аргументов шаблона;
- пакет параметров функции (function parameter pack) — соответственно, это параметр функции, принимающий любое (в т.ч. 0) количество аргументов функции;
- и раскрытие пакета (pack expansion) — это единственное, что можно сделать с пакетом параметров.
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); }
Свертки появились в С++14 и могут быть унарными и бинарными, правыми и левыми. Самое полное описание, как всегда, в Стандарте [C++17 8.1.6].
- оба типа пакета параметров можно раскрывать в оператор sizeof...
template <class ... Args> void foo(Args ... args) { const auto size1 = sizeof...(Args); const auto size2 = sizeof...(args); }
При раскрытии пакета явный эллипсис необходим, чтобы поддержать различные шаблоны (patterns) раскрытия и избежать при этом неоднозначности.
template <class ... Args>
void foo()
{
using OneTuple = std::tuple<std::tuple<Args>...>;
using NestTuple = std::tuple<std::tuple<Args...>>;
}
OneTuple
— это получается кортеж одноэлементных кортежей (std:tuple<std::tuple<int>>, std::tuple<double>>
), а NestTuple
— кортеж, состоящий из одного элемента — другого кортежа (std::tuple<std::tuple<int, double>>
).Пример реализации printf с помощью вариативных шаблонов
Как я уже упоминал, вариативные шаблоны создавались в том числе и как непосредственная замена вариативным функциям C. Сами авторы этих шаблонов предложили свою, очень простую, но типобезопасную версию
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
Есть сопоставление с эллипсисом — соответствующая функция проигрывает, нет сопоставления с эллипсисом — и шаблонная функция уступает нешаблонной.
Небольшое замечание о скорости вариативных шаблонных функций
В 2008 году Лоик Жоли (Loic Joly) подал в комитет по стандартизации C++ своё предложение N2772, в котором на практике показал, что вариативные шаблонные функции работают медленнее аналогичных функций, аргументом которых является список инициализации (
std::initializer_list
). И хотя это противоречило теоретическим обоснованиям самого автора, Жоли предложил реализовать std::min
, std::max
и std::minmax
именно с помощью списков инициализации, а не вариативных шаблонов.Но уже в 2009 году появилось опровержение. В тестах Жоли была обнаружена «серьезная ошибка» (кажется, даже им самим). Новые тесты (см. тут и тут) показали, что вариативные шаблонные функции все-таки быстрее, и иногда значительно. Что не удивительно, т.к. список инициализации делает копии своих элементов, а для вариативных шаблонов можно многое посчитать еще на этапе компиляции.
Тем не менее в C++11 и последующих стандартах
std::min
, std::max
и std::minmax
– это обычные шаблонные функции, произвольное число аргументов в которые передается через список инициализации.Краткое резюме и вывод
Итак, вариативные функции в стиле C:
- Не знают ни числа своих аргументов, ни их типов. Разработчик должен использовать часть аргументов функции для того, чтобы передать информацию об остальных.
- Неявно повышают типы неименованных аргументов (и последнего именованного). Если забыть об этом, получится неопределенное поведение.
- Сохраняют обратную совместимость с чистым C и потому не поддерживают передачу аргументов по ссылке.
- До C++11 не поддерживали аргументы не POD типов, а начиная с C++11 поддержка нетривиальных типов оставлена на усмотрение компилятора. Т.е. поведение кода зависит от компилятора и его версии.
Единственное допустимое использование вариативных функций — взаимодействие с C API в C++ коде. Для всего остального, включая SFINAE, есть вариативные шаблонные функции, которые:
- Знают количество и типы всех своих аргументов.
- Типобезопасны, не меняют типы своих аргументов.
- Поддерживают передачу аргументов в любом виде — по значению, по указателю, по ссылке, по универсальной ссылке.
- Как и любые другие C++ функции, не имеют ограничений на типы аргументов.
- При разрешении перегрузки рассматриваются в последнюю очередь (уступая только вариативным C функциям), как наименее специализированные.
Вариативные шаблонные функции могут быть более многословными по сравнению со своими аналогами в стиле C и иногда даже требовать своей перегруженной нешаблонной версии (рекурсивный обход аргументов). Их сложнее читать и писать. Но все это с лихвой окупается отсутствием перечисленных недостатков и наличием перечисленных же достоинств.
Ну и вывод получается простой: вариативные функции в C стиле остаются в C++ только из-за обратной совместимости, и они предлагают широкой выбор возможностей прострелить себе ногу. В современном C++ очень желательно не писать новые и по возможности не использовать уже существующие вариативные C функции. Вариативные же шаблонные функции принадлежат миру современного C++ и гораздо более безопасны. Используйте их.
Литература и источники
- P.J. Plauger, The Standard C Library
- Brian W. Kernighan and Dennis M. Ritchie, The C Programming Language, 1st Edition
- Brian W. Kernighan and Dennis M. Ritchie, The C Programming Language, 2nd Edition
- Стандарт C11, черновик N1570
- Стандарт C++98
- Стандарт C++03
- Стандарт C++11, черновик N3337
- Стандарт C++14, черновик N4296
- Стандарт C++17, черновик N4659
P. S.
В сети легко найти и скачать электронные версии упомянутых книг. Но я не уверен, что это будет легально, поэтому ссылок не даю.
Комментарии (11)
svr_91
16.11.2018 11:34Не понимаю, зачем в
printf на шаблонах и без рекурсии
std::vector. Разве нельзя как-нибудь так:
((printFmt(), std::cout << args), ..., (printFmt()));
Fyret Автор
16.11.2018 12:29Раскрытие пакета — это не макрос, оно не заменит
args...
на просто список через запятую. Я привел список того, как можно раскрывать пакет. В данном случае — в список инициализации. Ваш пример просто не скомпилируется.svr_91
16.11.2018 13:30Почему нет?
c++17Fyret Автор
16.11.2018 13:44Хм, проверю вечером. Тот g++, что у меня есть сейчас отказывается это компилировать.
Fyret Автор
16.11.2018 22:06Да, действительно так можно, это получается свертка. А я, когда пытался избавиться от вектора, раскрывал пакет в список инициализации и получал ошибку компиляции. Спасибо большое! Я обновлю функцию в статье.
snizovtsev
16.11.2018 22:42printf на вариативных шаблонах не сможет выдать compile-time ошибку несоответвия формата, т.к. шаблон (const char*) нельзя передать в constexpr функцию на проверку. Разве что хаком с _s из boost::hana, но он использует нестандартизированное расширение языка в GCC и Clang, да и компилироваться такая конструкция будет неспешно.
dev96
17.11.2018 03:29зато const char(&str)[strLen] или std::string_view может быть разобранным compile-time.
Amomum
17.11.2018 04:54По-моему, «type promotion» обычно переводят как «расширение типа»; по крайней мере, такой вариант в гугле все же попадается.
BratSinot
В примере с «повышаемыми» типами вы заменили float на double. double же тоже «повышаемый»? Есть long double.
Fyret Автор
Нет, все целочисленное ниже int повышается до int (со знаком или без), float — до double. [С++17 7.5 и 7.6]