En programación informática , las plantillas variádicas son plantillas que toman una cantidad variable de argumentos.
Las plantillas variádicas son compatibles con C++ (desde el estándar C++11 ) y el lenguaje de programación D.
La característica de plantilla variádica de C++ fue diseñada por Douglas Gregor y Jaakko Järvi [1] [2] y luego se estandarizó en C++11. Antes de C++11, las plantillas (clases y funciones) solo podían aceptar una cantidad fija de argumentos, que debían especificarse cuando se declaraba una plantilla por primera vez. C++11 permite que las definiciones de plantilla acepten una cantidad arbitraria de argumentos de cualquier tipo.
plantilla < typename ... Valores > clase tupla ; // toma cero o más argumentos
La clase de plantilla anterior tuple
aceptará cualquier cantidad de nombres de tipos como parámetros de plantilla. Aquí, se crea una instancia de la clase de plantilla anterior con tres argumentos de tipo:
tupla < int , std :: vector < int > , std :: mapa < std :: cadena , std :: vector < int >>> algún_nombre_de_instancia ;
El número de argumentos puede ser cero, por lo que también funcionará.tuple<> some_instance_name;
Si la plantilla variádica solo debe permitir un número positivo de argumentos, se puede utilizar esta definición:
plantilla < typename Primero , typename ... Resto > clase tupla ; // toma uno o más argumentos
Las plantillas variádicas también pueden aplicarse a funciones, lo que no solo proporciona un complemento seguro para funciones variádicas (como printf), sino que también permite que una función llamada con una sintaxis similar a printf procese objetos no triviales.
plantilla < typename ... Params > void my_printf ( const std :: string & str_format , Params ... parámetros );
El operador de puntos suspensivos (...) tiene dos funciones. Cuando aparece a la izquierda del nombre de un parámetro, declara un paquete de parámetros. Con el paquete de parámetros, el usuario puede vincular cero o más argumentos a los parámetros de plantilla variádicos. Los paquetes de parámetros también se pueden utilizar para parámetros que no sean de tipo. Por el contrario, cuando el operador de puntos suspensivos aparece a la derecha de un argumento de llamada de plantilla o función, descomprime los paquetes de parámetros en argumentos separados, como args...
en el cuerpo de printf
la siguiente figura. En la práctica, el uso de un operador de puntos suspensivos en el código hace que toda la expresión que precede a los puntos suspensivos se repita para cada argumento posterior descomprimido del paquete de argumentos, con las expresiones separadas por comas.
El uso de plantillas variádicas suele ser recursivo. Los parámetros variádicos en sí mismos no están fácilmente disponibles para la implementación de una función o clase. Por lo tanto, el mecanismo típico para definir algo como un printf
reemplazo variádico de C++11 sería el siguiente:
// caso base void my_printf ( const char * s ) { while ( * s ) { if ( * s == '%' ) { if ( * ( s + 1 ) == '%' ) ++ s ; else throw std :: runtime_error ( "cadena de formato no válida: argumentos faltantes" ); } estándar :: cout << * s ++ ; } } // plantilla recursiva < typename T , typename ... Args > void my_printf ( const char * s , T value , Args ... args ) { while ( * s ) { if ( * s == '%' ) { if ( * ( s + 1 ) != '%' ) { // pretende analizar el formato: solo funciona en cadenas de formato de 2 caracteres (%d, %f, etc.); falla con %5.4f s += 2 ; // imprime el valor std :: cout << value ; // se llama incluso cuando *s es 0 pero no hace nada en ese caso (e ignora los argumentos adicionales) my_printf ( s , args ...); return ; } ++ s ; } estándar :: cout << * s ++ ; } }
Esta es una plantilla recursiva. Observe que la versión de plantilla variádica my_printf
se llama a sí misma o (en caso de que args...
esté vacía) llama al caso base.
No existe un mecanismo simple para iterar sobre los valores de la plantilla variádica. Sin embargo, hay varias formas de traducir el paquete de argumentos en un único argumento que se pueda evaluar por separado para cada parámetro. Por lo general, esto dependerá de la sobrecarga de la función o, si la función simplemente puede elegir un argumento a la vez, utilizando un marcador de expansión tonto:
plantilla < typename ... Args > inline void pass ( Args && ...) {}
que se puede utilizar de la siguiente manera:
plantilla < typename ... Args > inline void expand ( Args && ... args ) { pass ( some_function ( args )...); } expandir ( 42 , "respuesta" , verdadero );
que se expandirá a algo como:
pasar ( alguna_función ( arg1 ), alguna_función ( arg2 ), alguna_función ( arg3 ) /* etc... */ );
El uso de esta función "pass" es necesario, ya que la expansión del paquete de argumentos se realiza separando los argumentos de la llamada de función mediante comas, que no son equivalentes al operador de coma. Por lo tanto, some_function(args)...;
nunca funcionará. Además, la solución anterior solo funcionará cuando el tipo de retorno de some_function
no sea void
. Además, las some_function
llamadas se ejecutarán en un orden no especificado, porque el orden de evaluación de los argumentos de la función no está definido. Para evitar el orden no especificado, se pueden utilizar listas de inicializadores encerradas entre llaves, que garantizan un estricto orden de evaluación de izquierda a derecha. Una lista de inicializadores requiere un void
tipo de retorno que no sea , pero se puede utilizar el operador de coma para obtener el resultado 1
de cada elemento de expansión.
struct pass { plantilla < nombre_tipo ... T > pass ( T ...) {} }; pasar {( alguna_funcion ( args ), 1 )...};
En lugar de ejecutar una función, se puede especificar una expresión lambda y ejecutarla en el lugar, lo que permite ejecutar secuencias arbitrarias de declaraciones en el lugar.
pasar{([&](){ std::cout << args << std::endl; }(), 1)...};
Sin embargo, en este ejemplo en particular no es necesaria una función lambda. Se puede utilizar en su lugar una expresión más común:
pasar{(std::cout << args << std::endl, 1)...};
Otra forma es utilizar la sobrecarga con "versiones de terminación" de funciones. Esto es más universal, pero requiere un poco más de código y más esfuerzo para crearlo. Una función recibe un argumento de algún tipo y el paquete de argumentos, mientras que la otra no recibe ninguno. (Si ambas tuvieran la misma lista de parámetros iniciales, la llamada sería ambigua: un paquete de parámetros variádico por sí solo no puede desambiguar una llamada). Por ejemplo:
void func () {} // versión de terminación plantilla < typename Arg1 , typename ... Args > void func ( const Arg1 & arg1 , const Args && ... args ) { proceso ( arg1 ); func ( args ...); // nota: ¡arg1 no aparece aquí! }
Si args...
contiene al menos un argumento, se redireccionará a la segunda versión; un paquete de parámetros puede estar vacío, en cuyo caso simplemente se redireccionará a la versión de terminación, que no hará nada.
Las plantillas variádicas también se pueden utilizar en una especificación de excepción, una lista de clases base o la lista de inicialización de un constructor. Por ejemplo, una clase puede especificar lo siguiente:
plantilla < typename ... BaseClasses > clase NombreClase : public BaseClasses ... { public : NombreClase ( BaseClasses && ... base_classes ) : BaseClasses ( base_classes )... {} };
El operador unpack replicará los tipos de las clases base de ClassName
, de modo que esta clase se derivará de cada uno de los tipos pasados. Además, el constructor debe tomar una referencia a cada clase base, para así inicializar las clases base de ClassName
.
En el caso de las plantillas de funciones, los parámetros variádicos se pueden reenviar. Si se combinan con referencias universales (ver arriba), esto permite un reenvío perfecto:
plantilla < nombre_tipo TypeToConstruct > struct SharedPtrAllocator { plantilla < nombre_tipo ... Args > std :: shared_ptr < TypeToConstruct > construcción_con_shared_ptr ( Args && ... parámetros ) { return std :: shared_ptr < TypeToConstruct > ( new TypeToConstruct ( std :: forward < Args > ( parámetros )...)); } };
Esto descomprime la lista de argumentos en el constructor de TypeToConstruct. La std::forward<Args>(params)
sintaxis reenvía perfectamente los argumentos como sus tipos adecuados, incluso con respecto a su carácter de valor r, al constructor. El operador de descompresión propagará la sintaxis de reenvío a cada parámetro. Esta función de fábrica en particular envuelve automáticamente la memoria asignada en un std::shared_ptr
para un grado de seguridad con respecto a las fugas de memoria.
Además, la cantidad de argumentos en un paquete de parámetros de plantilla se puede determinar de la siguiente manera:
plantilla < typename ... Args > struct SomeStruct { static const int tamaño = sizeof ...( Args ); };
La expresión SomeStruct<Type1, Type2>::size
dará como resultado 2, mientras que SomeStruct<>::size
dará como resultado 0.
La definición de plantillas variádicas en D es similar a su contraparte en C++:
plantilla VariadicTemplate ( Args ...) { /* Cuerpo */ }
Del mismo modo, cualquier argumento puede preceder a la lista de argumentos:
plantilla VariadicTemplate ( T , valor de cadena , símbolo de alias , Args ...) { /* Cuerpo */ }
Los argumentos variádicos son muy similares a las matrices constantes en su uso. Se pueden iterar, acceder a ellos mediante un índice, tienen una length
propiedad y se pueden dividir en segmentos . Las operaciones se interpretan en tiempo de compilación, lo que significa que los operandos no pueden ser valores en tiempo de ejecución (como los parámetros de función).
Todo lo que se conoce en tiempo de compilación se puede pasar como argumento variádico. Esto hace que los argumentos variádicos sean similares a los argumentos de alias de plantilla, pero más potentes, ya que también aceptan tipos básicos (char, short, int...).
A continuación se muestra un ejemplo que imprime la representación de cadena de los parámetros variádicos StringOf
y StringOf2
produce resultados iguales.
int estático s_int ; Estructura ficticia {} void main () { pragma ( msg , StringOf !( "Hola mundo" , uint , Dummy , 42 , s_int )); pragma ( msg , StringOf2 !( "Hola mundo" , uint , Dummy , 42 , s_int )); } plantilla StringOf ( Args ...) { enumeración StringOf = Args [ 0 ]. stringof ~ StringOf !( Args [ 1. .$]); } plantilla StringOf () { enumeración StringOf = "" ; } plantilla StringOf2 ( Args ...) { static if ( Args.length == 0 ) enumeración StringOf2 = "" ; de lo contrario enumeración StringOf2 = Args [ 0 ] .stringof ~ StringOf2 !( Args [ 1 .. $]); }
Salidas:
"Hola mundo"uintDummy42s_int"Hola mundo"uintDummy42s_int
Las plantillas variádicas se utilizan a menudo para crear una secuencia de alias, denominada AliasSeq. La definición de un AliasSeq es, en realidad, muy sencilla:
alias AliasSeq ( Args ...) = Args ;
Esta estructura permite manipular una lista de argumentos variádicos que se expandirán automáticamente. Los argumentos deben ser símbolos o valores conocidos en el momento de la compilación. Esto incluye valores, tipos, funciones o incluso plantillas no especializadas. Esto permite cualquier operación que se pueda esperar:
importar std . meta ; void main () { // Nota: AliasSeq no se puede modificar, y un alias no se puede volver a enlazar, por lo que necesitaremos definir nuevos nombres para nuestras modificaciones. alias numbers = AliasSeq !( 1 , 2 , 3 , 4 , 5 , 6 ); // Segmentación de alias lastHalf = numbers [$ / 2 .. $]; static assert ( lastHalf == AliasSeq !( 4 , 5 , 6 )); // Expansión automática de AliasSeq alias digits = AliasSeq !( 0 , numbers , 7 , 8 , 9 ); static assert ( digits == AliasSeq !( 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 )); // std.meta proporciona plantillas para trabajar con AliasSeq, como anySatisfy, allSatisfy, staticMap y Filter. alias evenNumbers = Filter !( isEven , digits ); static assert ( evenNumbers == AliasSeq !( 0 , 2 , 4 , 6 , 8 )); } plantilla isEven ( int número ) { enumeración isEven = ( 0 == ( número % 2 )); }
Para artículos sobre construcciones variádicas distintas de las plantillas