- PVSM.RU - https://www.pvsm.ru -
Началось все, как водится, с ошибки. Я первый раз работал с Java Native Interface [1] и делал в C++ части обертку над функцией, создающей Java объект. Эта функция — CallVoidMethod — вариативна, т.е. помимо указателя на среду JNI, указателя на тип создаваемого объекта и идентификатора вызываемого метода (в данном случае конструктора), она принимает произвольное число других аргументов. Что логично, т.к. эти другие аргументы передаются вызываемому методу на стороне Java, а методы могут быть разные, с разным числом аргументов любых типов.
Соответственно и свою обертку я тоже сделал вариативной. Для передачи произвольного числа аргументов в CallVoidMethod использовал va_list, потому что по-другому в данном случае никак. Да, так и отправил va_list в CallVoidMethod. И уронил JVM банальным segmentation fault.
За 2 часа я успел перепробовать несколько версий JVM, от 8-ой до 11-ой, потому что: во-первых это мой первый опыт с JVM [2], и в этом вопросе я StackOverflow доверял больше, чем себе, а во-вторых кто-то на StackOverflow посоветовал в таком случае использовать не OpenJDK, а OracleJDK, и не 8, а 10. И лишь потом я наконец заметил, что помимо вариативной CallVoidMethod есть CallVoidMethodV, которая произвольное число аргументов принимает через va_list.
Что мне больше всего не понравилось в этой истории, так это то, что я не сразу заметил разницу между эллипсисом (многоточием) и va_list. А заметив, не смог объяснить себе, в чем принципиальное отличие. Значит, надо разобраться и с эллипсисом, и с va_list, и (поскольку речь все-таки о C++) с вариативными шаблонами.
Стандарт C++ описывает только отличия своих требований от требований Стандарта С. О самих отличиях позже, а пока кратко перескажу, что говорит Стандарт С (начиная с C89).
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].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 [3] использует такой тип для 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++ занималась и занимается рабочая группа WG21 [4]. За основу еще в 1989 году был взял только что созданный Стандарт С89, который постепенно менялся, чтобы описывать собственно C++. В 1995 году поступило предложение N0695 [5] от Джона Микко (John Micco), в котором автор предлагал изменить ограничения для макросов va_*:
register переменных, то последний именованный аргумент вариативной функции может иметь этот класс хранения.
If the parameter
parmNis declared with… a type that is not compatible with the type that results after application of the default argument promotions, the behavior is undefined
надо заменить на
If the parameter
parmNis 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
Последний пункт я даже переводить не стал, чтобы поделиться своей болью. Во-первых, «повышение типа аргумента по умолчанию» в Стандарте C++ осталось [C++17 8.2.2/9]. И во-вторых, я долго ломал голову над смыслом этой фразы, сравнивал со Стандартом С, где все понятно. Только после прочтения N0695 я наконец понял: тут имеется в виду то же самое.
Тем не менее, все 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 выдал предупреждение.
./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 выдал предупреждение, но все-таки компиляцию выполнил.
./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 не должен быть ссылкой.
c:program files (x86)microsoft visual studio2017communityvctoolsmsvc14.15.26726includevadefs.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, а если этот код все-таки будет выполнен, то программа завершится с ошибкой.
./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 выдает точно такие же результаты, только без предупреждений, даже с /Wall.
Тут можно было бы предположить, что разница между 32 и 64 битами обусловлена тем, что в первом случае ABI передает вызванной функции все аргументы через стек, а во втором первые четыре (Windows) или шесть (Linux) аргументов через регистры процессора, остальные — через стек [wiki [6]]. Но нет, если вызывать 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 типом, и предупреждает, что при запуске программа упадет.
./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 предупреждает, что использование в данном случае типов с нетривиальным конструкторов непереносимо.
d:my documentsvisual studio 2017projectstesttestmain.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++ их поддержку убирать не собираются. Значит, есть какая-то польза от этих функций? Ну есть.
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(...). Я бы предпочел изменить пару строчек и использовать современную альтернативу вариативным функциям, лишенную всех их недостатков.
Идея вариативных шаблонов была предложена Дугласом Грегором (Douglas Gregor), Яакко Ярви (Jaakko Järvi) и Гэри Пауэллом (Gary Powell) еще в 2004 году, т.е. за 7 лет до принятия стандарта C++11, в котором эти вариативные шаблоны и были официально поддержаны. В Стандарт вошла третья ревизия их предложения за номером N2080 [8].
С самого начала вариативные шаблоны создавались, чтобы у программистов была возможность создавать типобезопасные (и переносимые!) функции от произвольного числа аргументов. Другая цель — упростить поддержку шаблонов классов с переменным числом параметров, но сейчас речь идет только о вариативных функциях.
Вариативные шаблоны принесли в 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);
}
Свертки появились в С++14 и могут быть унарными и бинарными, правыми и левыми. Самое полное описание, как всегда, в Стандарте [C++17 8.1.6].
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>>).
Как я уже упоминал, вариативные шаблоны создавались в том числе и как непосредственная замена вариативным функциям 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]()
{
if (fmtIndex >= fmt.size())
throw std::runtime_error("extra arguments provided to printf");
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;
}
}
}
};
std::vector<int>{(printFmt(), std::cout << args, 0)..., (printFmt(), 0)};
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 году Лоик Жоли (Loïc Joly) подал в комитет по стандартизации C++ своё предложение N2772 [9], в котором на практике показал, что вариативные шаблонные функции работают медленнее аналогичных функций, аргументом которых является список инициализации (std::initializer_list). И хотя это противоречило теоретическим обоснованиям самого автора, Жоли предложил реализовать std::min, std::max и std::minmax именно с помощью списков инициализации, а не вариативных шаблонов.
Но уже в 2009 году появилось опровержение. В тестах Жоли была обнаружена «серьезная ошибка» (кажется, даже им самим). Новые тесты (см. тут [10] и тут [11]) показали, что вариативные шаблонные функции все-таки быстрее, и иногда значительно. Что не удивительно, т.к. список инициализации делает копии своих элементов, а для вариативных шаблонов можно многое посчитать еще на этапе компиляции.
Тем не менее в C++11 и последующих стандартах std::min, std::max и std::minmax – это обычные шаблонные функции, произвольное число аргументов в которые передается через список инициализации.
Итак, вариативные функции в стиле C:
Единственное допустимое использование вариативных функций — взаимодействие с C API в C++ коде. Для всего остального, включая SFINAE, есть вариативные шаблонные функции, которые:
Вариативные шаблонные функции могут быть более многословными по сравнению со своими аналогами в стиле C и иногда даже требовать своей перегруженной нешаблонной версии (рекурсивный обход аргументов). Их сложнее читать и писать. Но все это с лихвой окупается отсутствием перечисленных недостатков и наличием перечисленных же достоинств.
Ну и вывод получается простой: вариативные функции в C стиле остаются в C++ только из-за обратной совместимости, и они предлагают широкой выбор возможностей прострелить себе ногу. В современном C++ очень желательно не писать новые и по возможности не использовать уже существующие вариативные C функции. Вариативные же шаблонные функции принадлежат миру современного C++ и гораздо более безопасны. Используйте их.
В сети легко найти и скачать электронные версии упомянутых книг. Но я не уверен, что это будет легально, поэтому ссылок не даю.
Автор: Fyret
Источник [21]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/299295
Ссылки в тексте:
[1] Java Native Interface: https://ru.wikipedia.org/wiki/Java_Native_Interface
[2] JVM: https://ru.wikipedia.org/wiki/Java_Virtual_Machine
[3] SystemV ABI для AMD64: https://www.uclibc.org/docs/psABI-x86_64.pdf
[4] WG21: http://www.open-std.org/jtc1/sc22/wg21/
[5] N0695: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/1995/N0695.pdf
[6] wiki: https://en.wikipedia.org/wiki/X86_calling_conventions#x86-64_calling_conventions
[7] SFINAE: https://ru.wikipedia.org/wiki/SFINAE
[8] N2080: http://www.open-std.org/Jtc1/sc22/wg21/docs/papers/2006/n2080.pdf
[9] N2772: http://www.open-std.org/Jtc1/sc22/wg21/docs/papers/2008/n2772.pdf
[10] тут: https://nd.home.xs4all.nl/dekkerware/issues/n2772_fix/draft_november_2009.htm
[11] тут: https://nd.home.xs4all.nl/dekkerware/issues/n2772_fix/
[12] P.J. Plauger, The Standard C Library: https://www.amazon.com/Standard-C-Library-P-J-Plauger/dp/0131315099
[13] Brian W. Kernighan and Dennis M. Ritchie, The C Programming Language, 1st Edition: https://www.amazon.com/Programming-Language-Brian-W-Kernighan/dp/0131101633
[14] Brian W. Kernighan and Dennis M. Ritchie, The C Programming Language, 2nd Edition: https://www.amazon.com/Programming-Language-2nd-Brian-Kernighan/dp/0131103628
[15] Стандарт C11, черновик N1570: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
[16] Стандарт C++98: http://www.lirmm.fr/~ducour/Doc-objets/ISO+IEC+14882-1998.pdf
[17] Стандарт C++03: https://cs.nyu.edu/courses/fall11/CSCI-GA.2110-003/documents/c++2003std.pdf
[18] Стандарт C++11, черновик N3337: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf
[19] Стандарт C++14, черновик N4296: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4296.pdf
[20] Стандарт C++17, черновик N4659: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf
[21] Источник: https://habr.com/post/430064/?utm_campaign=430064
Нажмите здесь для печати.