C ++: una sesión de arqueología espontánea y por qué no debes usar funciones variables al estilo de C

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).

  • Puede declarar una función que tome un número arbitrario de argumentos. Es decir Una función puede tener más argumentos que parámetros. Para hacer esto, la lista de sus parámetros debe terminar con puntos suspensivos, pero al menos un parámetro fijo [C11 6.9.1 / 8] también debe estar presente:

     void foo(int parm1, int parm2, ...); 
  • La información sobre el número y los tipos de argumentos correspondientes a los puntos suspensivos no se pasa a la función misma. Es decir después del último parámetro nombrado ( parm2 en el ejemplo anterior) [C11 6.7.6.3/9] .
  • Para acceder a estos argumentos, debe usar el tipo va_list declarado en el <stdarg.h> y 4 macros (3 antes del estándar C11): va_start , va_arg , va_end y va_copy (comenzando con C11) [C11 7.16] .

    Por ejemplo
     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; } 

    Sí, la función no sabe cuántos argumentos tiene. Ella necesita pasar de alguna manera este número. En este caso, a través de un único argumento con nombre (otra opción común es pasar NULL como último argumento, como en execl , o 0).
  • El último argumento nombrado no puede tener una clase de almacenamiento de register ; no puede ser una función o una matriz. De lo contrario, comportamiento indefinido [C11 7.16.1.4/4] .
  • Además, para el último argumento nombrado y para todos los sin nombre, se aplica la " promoción de argumento por defecto " ( promoción de argumento por defecto ; si hay una buena traducción de este concepto al ruso, con mucho gusto lo uso). Esto significa que si el argumento tiene el tipo char , short (con o sin signo) o float , entonces se debe acceder a los parámetros correspondientes como int , int (con o sin signo) o double . De lo contrario, comportamiento indefinido [C11 7.16.1.1/2] .
  • Sobre el tipo va_list solo se dice que está declarado en <stdarg.h> y está completo (es decir, se conoce el tamaño de un objeto de este tipo) [C11 7.16 / 3] .

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 por
    Si 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


  1. 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); } 

  2. 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); } 

  3. 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); } 

  4. 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::maxy std::minmaxprecisamente 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::maxy std::minmaxson 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.

Source: https://habr.com/ru/post/es430064/


All Articles