- 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
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 undefined
надо заменить на
If 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
Последний пункт я даже переводить не стал, чтобы поделиться своей болью. Во-первых, «повышение типа аргумента по умолчанию» в Стандарте 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
Нажмите здесь для печати.