Todo comenzó, como siempre, con un error. Esta es la primera vez que trabajo con la
interfaz nativa de Java y en la parte de C ++ envolví una función que crea un objeto Java. Esta función,
CallVoidMethod
, es variable, es decir Además de un puntero al entorno
JNI , un puntero al tipo de objeto a crear y un identificador para el método llamado (en este caso, el constructor), toma un número arbitrario de otros argumentos. Lo cual es lógico, porque estos otros argumentos se pasan al método llamado en el lado de Java, y los métodos pueden ser diferentes, con un número diferente de argumentos de cualquier tipo.
En consecuencia, también hice que mi envoltorio fuera variable. Para pasar un número arbitrario de argumentos a
CallVoidMethod
utilicé
va_list
, porque es diferente en este caso. Sí, eso es lo que
va_list
envió a
CallVoidMethod
. Y dejó caer la falla de segmentación banal JVM.
En 2 horas logré probar varias versiones de la JVM, del 8 al 11, porque: en primer lugar, esta es mi primera experiencia con la
JVM , y en este asunto confié en StackOverflow más que en mí mismo, y en segundo lugar, alguien luego, en StackOverflow, aconsejé en este caso no usar OpenJDK, sino OracleJDK, y no 8, sino 10. Y solo entonces finalmente noté que, además de la variable
CallVoidMethod
hay
CallVoidMethodV
, que toma un número arbitrario de argumentos a través de
va_list
.
Lo que no me gustó más de esta historia fue que no noté de inmediato la diferencia entre los puntos suspensivos (puntos suspensivos) y
va_list
. Y habiéndome dado cuenta, no podía explicarme cuál era la diferencia fundamental. Entonces, tenemos que lidiar con puntos suspensivos, y con
va_list
, y (ya que todavía estamos hablando de C ++) con plantillas variables.
¿Qué pasa con los puntos suspensivos y va_list se dice en el Estándar?
El Estándar C ++ describe solo las diferencias entre sus requisitos y los del Estándar C. Las diferencias en sí se discutirán más adelante, pero por ahora explicaré brevemente lo que dice el Estándar C (comenzando con C89).
Por qué Pero porque!
No hay muchos tipos en C. ¿Por qué se declara
va_list
en el Estándar, pero no se dice nada sobre su estructura interna?
¿Por qué necesitamos puntos suspensivos si se puede pasar un número arbitrario de argumentos a una función a través de
va_list
? Se podría decir ahora: "como azúcar sintáctico", pero hace 40 años, estoy seguro, no había tiempo para el azúcar.
Philip James Plauger
Phillip James Plauger en el libro
The Standard C library - 1992 - dice que inicialmente C fue creado exclusivamente para computadoras PDP-11. Y allí fue posible clasificar todos los argumentos de la función usando aritmética de puntero simple. El problema apareció con la popularidad de C y la transferencia del compilador a otras arquitecturas. La primera edición de
The C Programming Language por Brian Kernighan y Dennis Ritchie - 1978 - declara explícitamente:
Por cierto, no hay una forma aceptable de escribir una función portátil de un número arbitrario de argumentos, porque No hay una forma portátil para que la función llamada descubra cuántos argumentos se le pasaron cuando se llamó. ... printf
, la función de lenguaje C más típica de un número arbitrario de argumentos, ... no es portátil y debe implementarse para cada sistema.
Este libro describe
printf
, pero aún no tiene
vprintf
, y no menciona el tipo y las macros
va_*
. Aparecen en la segunda edición del lenguaje de programación C (1988), y este es el mérito del comité para el desarrollo del primer estándar C (C89, también conocido como ANSI C). El comité agregó el
<stdarg.h>
al Estándar, tomando como base
<varargs.h>
, creado por Andrew Koenig con el objetivo de aumentar la portabilidad del sistema operativo UNIX.
va_*
decidió dejar
va_*
macros como macros para que sea más fácil para los compiladores existentes admitir el nuevo estándar.
Ahora, con el advenimiento de C89 y la familia
va_*
, se ha hecho posible crear funciones variables portátiles. Y aunque la estructura interna de esta familia todavía no se describe de ninguna manera, y no hay requisitos para ello, ya está claro por qué.
Por pura curiosidad, puede encontrar ejemplos de la implementación de
<stdarg.h>
. Por ejemplo, la misma "Biblioteca estándar de C" proporciona un ejemplo para
Borland Turbo C ++ :
<stdarg.h> de 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
El
ABI SystemV mucho más nuevo
para AMD64 usa este tipo para
va_list
:
va_list de SystemV ABI AMD64 typedef struct { unsigned int gp_offset; unsigned int fp_offset; void *overflow_arg_area; void *reg_save_area; } va_list[1];
En general, podemos decir que el tipo y las macros
va_*
proporcionan una interfaz estándar para atravesar argumentos de una función variable, y su implementación por razones históricas depende del compilador, las plataformas de destino y la arquitectura. Además, una elipsis (es decir, funciones variables en general) apareció en C antes que
va_list
(es decir, el encabezado
<stdarg.h>
). Y
va_list
no se creó para reemplazar los puntos suspensivos, sino para permitir a los desarrolladores escribir sus funciones variables portátiles.
C ++ mantiene en gran medida la compatibilidad con C, por lo que todo lo anterior se aplica a él. Pero también hay características.
Funciones variables en C ++
El grupo de trabajo
WG21 ha estado involucrado en el desarrollo del estándar C ++. En 1989, se tomó como base el recién creado Estándar C89, que cambió gradualmente para describir el propio C ++. En 1995,
se recibió la propuesta
N0695 de
John Micco , en la cual el autor sugirió cambiar las restricciones para las macros
va_*
:
- Porque C ++, a diferencia de C, le permite obtener la dirección de
register
de las variables, luego el último argumento nombrado de una función variable puede tener esta clase de almacenamiento.
- Porque los enlaces que aparecieron en C ++ violan la regla no escrita de las funciones variables de C (el tamaño del parámetro debe coincidir con el tamaño de su tipo declarado), entonces el último argumento nombrado no puede ser un enlace. De lo contrario, comportamiento vago.
- Porque en C ++ no existe el concepto de " elevar el tipo de argumento por defecto ", entonces la frase
Si el parámetro parmN
se declara con ... un tipo que no es compatible con el tipo que resulta después de la aplicación de las promociones de argumento predeterminadas, el comportamiento es indefinido
debe ser reemplazado porSi el parámetro parmN
se declara con ... un tipo que no es compatible con el tipo que resulta al pasar un argumento para el que no hay parámetro, el comportamiento es indefinido
Ni siquiera traduje el último punto para compartir mi dolor. Primero, la "
escalada de tipo de argumento predeterminado " en C ++ Standard permanece
[C ++ 17 8.2.2 / 9] . Y en segundo lugar, durante mucho tiempo me pregunté el significado de esta frase, en comparación con el Estándar C, donde todo está claro. Solo después de leer N0695 finalmente entendí: quiero decir lo mismo.
Sin embargo, los 3 cambios fueron adoptados
[C ++ 98 18.7 / 3] . De vuelta en C ++, el requisito de que una función variable tenga al menos un parámetro con nombre (en este caso no puede acceder a los demás, pero más sobre eso más adelante) ha desaparecido, y la lista de tipos válidos de argumentos sin nombre se ha complementado con punteros a los miembros de la clase y tipos de
POD .
El estándar C ++ 03 no trajo ningún cambio a las funciones variacionales. C ++ 11 comenzó a convertir un argumento sin nombre de tipo
std::nullptr_t
a
void*
y permitió a los compiladores, a su discreción, admitir tipos con constructores y destructores no triviales
[C ++ 11 5.2.2 / 7] . C ++ 14 permitió el uso de funciones y matrices como el último parámetro nombrado
[C ++ 14 18.10 / 3] , y C ++ 17 prohibió el uso de la expansión del paquete de parámetros (
expansión del paquete ) y las variables capturadas por el lambda
[C ++ 17 21.10.1 / 1] .
Como resultado, C ++ agregó funciones variadas a sus dificultades. Solo vale la pena el soporte de tipo no especificado con constructores / destructores no triviales. A continuación, intentaré reducir todas las características no obvias de las funciones variables en una lista y complementarla con ejemplos específicos.
Cómo usar funciones variables de manera fácil e incorrecta
- Es incorrecto declarar el último argumento nombrado con un tipo promocionado, es decir
char
, char
signed char
, unsigned char
, singed short
, unsigned short
o float
. El resultado según la Norma será un comportamiento indefinido.
Código inválido void foo(float n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); }
De todos los compiladores que tenía a mano (gcc, clang, MSVC), solo clang emitió una advertencia.
Advertencia de argot ./test.cpp:7:18: warning: passing an object that undergoes default argument promotion to 'va_start' has undefined behavior [-Wvarargs] va_start(va, n); ^
Y aunque en todos los casos el código compilado se comportó correctamente, no debe contar con él.
Estara bien void foo(double n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); }
- Es incorrecto declarar el último argumento nombrado como referencia. Cualquier enlace El estándar en este caso también promete un comportamiento indefinido.
Código inválido 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 compiló este código sin un solo comentario. lang 6.0.0 emitió una advertencia, pero aún así la compiló.
Advertencia de argot ./test.cpp:7:18: warning: passing an object of reference type to 'va_start' has undefined behavior [-Wvarargs] va_start(va, n); ^
En ambos casos, el programa funcionó correctamente (por suerte, no puede confiar en él). Pero MSVC 19.15.26730 se distinguió: se negó a compilar el código, porque va_start
argumento va_start
no va_start
ser una referencia.
Error de MSVC 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
Bueno, la opción correcta se ve, por ejemplo, así void foo(int* n, ...) { va_list va; va_start(va, n); std::cout << va_arg(va, int) << std::endl; va_end(va); }
- Es incorrecto solicitar
va_arg
elevar el tipo - char
, short
o float
.
Código inválido #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; }
Es más interesante aquí. gcc at compilation advierte que es necesario usar double
lugar de float
, y si este código aún se ejecuta, el programa terminará con un error.
Advertencia de CCG ./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
De hecho, el programa se bloquea con una queja sobre una instrucción no válida.
Un análisis de volcado muestra que el programa recibió una señal SIGILL. Y también muestra la estructura de va_list
. Para 32 bits esto es
va = 0xfffc6918 ""
es decir va_list
es solo char*
. Para 64 bits:
va = {{gp_offset = 16, fp_offset = 48, overflow_arg_area = 0x7ffef147e7e0, reg_save_area = 0x7ffef147e720}}
es decir exactamente lo que se describe en SystemV ABI AMD64.
El sonido metálico en la compilación advierte sobre un comportamiento indefinido y también sugiere reemplazar el float
por el double
.
Advertencia de argot ./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; ^~~~~
Pero el programa ya no se bloquea, la versión de 32 bits produce:
1 0 1073741824
64 bit:
1 0 3
MSVC produce exactamente los mismos resultados, solo sin previo aviso, incluso con /Wall
.
Aquí se podría suponer que la diferencia entre 32 y 64 bits se debe al hecho de que en el primer caso, el ABI pasa todos los argumentos a través de la pila a la función llamada, y en el segundo, los primeros cuatro (Windows) o seis (Linux) argumentos a través de los registros del procesador, el resto a través de apilar [ wiki ]. Pero no, si llama a foo
no con 4 argumentos, sino con 19, y los genera de la misma manera, el resultado será el mismo: desorden completo en la versión de 32 bits y ceros para todos los float
en la de 64 bits. Es decir el punto es, por supuesto, en ABI, pero no en el uso de registros para pasar argumentos.
Bueno, claro, por supuesto, hacerlo 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); }
- Es incorrecto pasar una instancia de una clase con un constructor o destructor no trivial como argumento sin nombre. A menos que, por supuesto, el destino de este código lo entusiasme al menos un poco más que "compilar y ejecutar aquí y ahora".
Código inválido #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 es el más estricto de todos Simplemente se niega a compilar este código porque el segundo argumento, va_arg
no va_arg
un tipo de POD, y advierte que el programa se va_arg
en el inicio.
Advertencia de argot ./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); ^
Así será, si aún compila con el indicador -Wno-non-pod-varargs
.
MSVC advierte que el uso de tipos con constructores no triviales en este caso no es portátil.
Advertencia de MSVC d:\my documents\visual studio 2017\projects\test\test\main.cpp(31): warning C4840: "Bar"
Pero el código se compila y se ejecuta correctamente. Lo siguiente se obtiene en la consola:
Resultado de lanzamiento Bar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor
Es decir se crea una copia solo al momento de llamar a va_arg
, y resulta que el argumento se pasa por referencia. De alguna manera no es obvio, pero la Norma lo permite.
gcc 6.3.0 compila sin un solo comentario. La salida es la misma:
Resultado de lanzamiento Bar default ctor Cafe default ctor Before va_arg Bar copy ctor Bar dtor Cafe dtor Bar dtor
gcc 7.3.0 tampoco advierte sobre nada, pero el comportamiento está cambiando:
Resultado de lanzamiento 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
Es decir Esta versión del compilador pasa argumentos por valor, y cuando se llama, va_arg
hace otra copia. Sería divertido buscar esta diferencia al cambiar de la sexta a la séptima versión de gcc si los constructores / destructores tienen efectos secundarios.
Por cierto, si pasa explícitamente y solicita una referencia a la clase:
Otro código equivocado 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; }
entonces todos los compiladores arrojarán un error. Según lo requiera la Norma.
En general, si realmente lo desea, es mejor pasar los argumentos por puntero.
Como este 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; }
Resolución de sobrecarga y funciones variables
Por un lado, todo es simple: la coincidencia con puntos suspensivos es peor que la coincidencia con un argumento con nombre normal, incluso en el caso de una conversión de tipo estándar o definida por el usuario.
Ejemplo de sobrecarga #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; }
Resultado de lanzamiento $ ./test Ordinary function Ordinary function C variadic function
Pero esto solo funciona hasta que la llamada a
foo
sin argumentos deba considerarse por separado.
Llama a foo sin argumentos #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; }
Salida del compilador ./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() ^~~
Todo está de acuerdo con el Estándar: no hay argumentos, no hay comparación con los puntos suspensivos, y cuando se resuelve la sobrecarga, la función variante no es peor que la habitual.
Sin embargo, ¿cuándo vale la pena usar funciones variables?
Bueno, las funciones variantes a veces no se comportan de manera muy obvia y en el contexto de C ++ pueden resultar fácilmente poco portables. Hay muchos consejos en Internet como "No crear o usar funciones variables de C", pero no van a eliminar su soporte del Estándar C ++. Entonces, ¿hay algún beneficio en estas características? Bueno ahi.
- El caso más común y obvio es la compatibilidad con versiones anteriores. Aquí incluiré el uso de bibliotecas C de terceros (mi caso con JNI) y la provisión de la API C para la implementación de C ++.
- SFINAE Aquí, es muy útil que en C ++ una función variable no tenga que tener argumentos nombrados, y que al resolver funciones sobrecargadas, una función variable se considere la última (si hay al menos un argumento). Y como cualquier otra función, una función variable solo puede declararse, pero nunca llamarse.
Ejemplo 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; };
Aunque en C ++ 14 puedes hacerlo un poco diferente.
Otro ejemplo 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); };
Y en este caso ya es necesario mirar con qué argumentos se puede llamar detect(...)
. Preferiría cambiar un par de líneas y usar una alternativa moderna a las funciones variables, desprovistas de todas sus deficiencias.
Plantillas variantes o cómo crear funciones a partir de un número arbitrario de argumentos en C ++ moderno
Douglas Gregor, Jaakko Järvi y Gary Powell propusieron la idea de plantillas variables en 2004, es decir. 7 años antes de la adopción del estándar C ++ 11, en el que estas plantillas variables fueron oficialmente compatibles.
La Norma incluyó una tercera revisión de su propuesta, N2080 .Desde el principio, se crearon plantillas variables para que los programadores tuvieran la oportunidad de crear funciones de tipo seguro (¡y portátil!) A partir de un número arbitrario de argumentos. Otro objetivo es simplificar el soporte para plantillas de clase con un número variable de parámetros, pero ahora solo estamos hablando de funciones variables.Las plantillas variables trajeron tres conceptos nuevos a C ++ [C ++ 17 17.5.3] :- parámetros de plantilla de paquete ( paquete de parámetro de plantilla ) - es una plantilla de parámetros, en lugar de la que es posible transferir cualquier (incluyendo 0) número de argumento de plantilla;
- un paquete de parámetros de función (paquete de parámetros de función ): en consecuencia, este es un parámetro de función que toma cualquier (incluido 0) número de argumentos de función;
- y la expansión del paquete ( expansión del paquete ) es lo único que se puede hacer con el paquete de parámetros.
Ejemplo template <class ... Args> void foo(const std::string& format, Args ... args) { printf(format.c_str(), args...); }
class ... Args
— ,
Args ... args
— ,
args...
— .
En el Estándar mismo se proporciona una lista completa de dónde y cómo se pueden expandir los paquetes de parámetros [C ++ 17 17.5.3 / 4] . Y en el contexto de la discusión de las funciones variables, es suficiente decir que:El paquete de parámetros de función se puede expandir a la lista de argumentos de otra función template <class ... Args> void bar(const std::string& format, Args ... args) { foo<Args...>(format.c_str(), args...); }
o a la lista de inicialización template <class ... Args> void foo(const std::string& format, Args ... args) { const auto list = {args...}; }
o a la lista de captura lambda template <class ... Args> void foo(const std::string& format, Args ... args) { auto lambda = [&format, args...] () { printf(format.c_str(), args...); }; lambda(); }
otro paquete de parámetros de función puede expandirse en una expresión de convolución template <class ... Args> int foo(Args ... args) { return (0 + ... + args); }
Las convoluciones aparecieron en C ++ 14 y pueden ser unarias y binarias, derecha e izquierda. La descripción más completa, como siempre, está en el Estándar [C ++ 17 8.1.6] .
ambos tipos de paquetes de parámetros se pueden ampliar en el operador sizeof ... template <class ... Args> void foo(Args ... args) { const auto size1 = sizeof...(Args); const auto size2 = sizeof...(args); }
Al revelar el paquete de puntos suspensivos explícita se necesita para apoyar las distintas plantillas ( patrones ) la divulgación y para evitar esta ambigüedad.Por ejemplo 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>>
).
Ejemplo de implementación de printf usando plantillas variables
Como ya mencioné, las plantillas variables también se crearon como un reemplazo directo para las funciones variables de C. Los autores de estas plantillas propusieron su versión muy simple pero segura de tipos printf
, una de las primeras funciones variables en C.printf en plantillas 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"); }
Sospecho que apareció este patrón de enumeración de argumentos variables, a través de una llamada recursiva de funciones sobrecargadas. Pero todavía prefiero la opción sin recurrencia.printf en plantillas y sin recursividad 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"); }
Resolución de sobrecarga y funciones de plantilla variable
Al resolver, estas funciones variadas se consideran, después de otras, como estándar y menos especializadas. Pero no hay problema en el caso de una llamada sin argumentos.Ejemplo de sobrecarga #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; }
Resultado de lanzamiento $ ./test Ordinary function Ordinary function without arguments Template function Template variadic function
Cuando se resuelve la sobrecarga, una función de plantilla variable solo puede omitir una función de variable C (aunque ¿por qué mezclarlas?). Excepto, por supuesto! - Llamada sin argumentos.Llama sin argumentos #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; }
Resultado de lanzamiento $ ./test Template variadic function C variadic function
Hay una comparación con puntos suspensivos: la función correspondiente pierde, no hay comparación con puntos suspensivos, y la función de plantilla es inferior a la que no es de plantilla.Una nota rápida sobre la velocidad de las funciones de plantilla variable
En 2008, Loïc Joly presentó su propuesta N2772 al Comité de Normalización de C ++ , en el que demostró en la práctica que las funciones de plantilla variable funcionan más lentamente que funciones similares, cuyo argumento es la lista de inicialización ( std::initializer_list
). Y aunque esto contradecía las justificaciones teóricas del propio autor, Joli propuso implementarlo std::min
, std::max
y std::minmax
precisamente con la ayuda de listas de inicialización, y no con plantillas variables.Pero ya en 2009, apareció una refutación. En las pruebas de Joli, se descubrió un "grave error" (parece, incluso para sí mismo). Nuevas pruebas (ver aquí y aquí) mostraron que las funciones de plantilla variable son aún más rápidas y, a veces, significativamente. Lo cual no es sorprendente ya que la lista de inicialización hace copias de sus elementos, y para plantillas variables puede contar mucho en la etapa de compilación.Sin embargo, en C ++ 11 y estándares posteriores std::min
, std::max
y std::minmax
son funciones de plantilla ordinarias, se pasa un número arbitrario de argumentos a través de la lista de inicialización.Breve resumen y conclusión
Entonces, funciones variables de estilo C:- No conocen ni el número de sus argumentos ni sus tipos. El desarrollador debe usar parte de los argumentos de la función para pasar información sobre el resto.
- Levanta implícitamente los tipos de argumentos sin nombre (y el último nombre). Si te olvidas de eso, obtienes un comportamiento vago.
- Mantienen la compatibilidad con versiones anteriores de C puro y, por lo tanto, no admiten pasar argumentos por referencia.
- Antes de C ++ 11, no se admitían argumentos que no fueran de tipos POD , y desde C ++ 11, el soporte para tipos no triviales se dejaba a discreción del compilador. Es decir El comportamiento del código depende del compilador y su versión.
El único uso permitido de funciones variables es interactuar con la API de C en código C ++. Para todo lo demás, incluido SFINAE , hay funciones de plantilla variables que:- Conozca el número y los tipos de todos sus argumentos.
- Escriba safe, no cambie los tipos de sus argumentos.
- Admiten pasar argumentos en cualquier forma: por valor, por puntero, por referencia, por enlace universal.
- Al igual que cualquier otra función de C ++, no hay restricciones en los tipos de argumentos.
- ( C ), .
Las funciones de plantilla variable pueden ser más detalladas en comparación con sus contrapartes de estilo C y, a veces, incluso requieren su propia versión sobrecargada sin plantilla (recorrido recursivo de argumentos). Son más difíciles de leer y escribir. Pero todo esto está más que pagado por la ausencia de las deficiencias enumeradas y la presencia de las ventajas enumeradas.Bueno, la conclusión es simple: las funciones variadas en el estilo C permanecen en C ++ solo debido a la compatibilidad con versiones anteriores, y ofrecen una amplia gama de opciones para dispararle a la pierna. En C ++ moderno, es muy recomendable no escribir otros nuevos y, si es posible, no utilizar las funciones de C variables existentes. Las funciones de plantilla variable pertenecen al mundo de C ++ moderno y son mucho más seguras. Úsalos.Literatura y Fuentes
PS
Es fácil encontrar y descargar versiones electrónicas de los libros mencionados en la red. Pero no estoy seguro de que sea legal, así que no doy enlaces.