Articulo de referencia

Plantilla variádica

En programación informática , las plantillas variádicas o genéricos de variación son plantillas que aceptan un número variable de parámetros de tipo. Existen en varios lenguajes...

En programación informática , las plantillas variádicas o genéricos de variación son plantillas que aceptan un número variable de parámetros de tipo. Existen en varios lenguajes de programación y se utilizan para crear genéricos con seguridad de tipos y un número variable de tipos. Son, en cierto modo, similares a las funciones variádicas o macros variádicas .

Las plantillas variádicas son compatibles con C++ (desde el estándar C++11 ) y el lenguaje de programación D. Si bien otros lenguajes, como Java o C#, ofrecen compatibilidad con funciones variádicas con tipado seguro , no ofrecen la compatibilidad con genéricos variádicos que sí ofrecen C++ y D.

C++

Antes de la introducción de las plantillas variádicas en C++, las funciones variádicas solo podían implementarse utilizando macros variádicasva_list de C , las cuales carecen de seguridad de tipos. La característica de plantillas variádicas de C++ fue diseñada por Douglas Gregor y Jaakko Järvi [ 1 ] [ 2 ] y posteriormente se estandarizó en C++11. Antes de C++11, las plantillas (clases y funciones) solo podían aceptar un número fijo de argumentos, que debía especificarse al declarar una plantilla por primera vez. C++11 permite que las definiciones de plantillas acepten un número arbitrario de argumentos de cualquier tipo. Por lo tanto, la única forma de obtener funciones variádicas con seguridad de tipos es mediante plantillas variádicas sobre funciones, ya que C++ carece de funciones variádicas sin plantillas como las de Java y C#.

// toma cero o más argumentos plantilla < typename ... Ts > clase Tuple ;

La clase plantilla anterior Tuple<Ts...>(que representa un tipo de producto o tupla) aceptará cualquier número de nombres de tipo como parámetros de plantilla . Aquí, se instancia una instancia de la clase plantilla anterior con tres argumentos de tipo:

using String = std :: string ; template < typename T > using Vector = std :: vector < T > ; template < typename K , typename V > using TreeMap = std :: map < K , V > ;Tupla < int , Vector < int > , TreeMap < String , Vector < int >>> t ;

El número de argumentos puede ser cero, por lo que también funcionará.Tuple<>t;

Si la plantilla variádica solo debe permitir un número positivo de argumentos, entonces se puede utilizar esta definición:

// toma uno o más argumentos template < typename First , typename ... Rest > class Tuple ;

Una implementación común de una tupla utiliza la descomposición recursiva de la siguiente manera:

// Plantilla de declaración anticipada < typename ... Ts > clase Tuple ;// Plantilla de caso vacío <> clase Tuple <> {};// Plantilla de caso recursivo < typename Head , typename ... Tail > clase Tuple < Head , Tail ... > { private : Head head ; Tuple < Tail ... > tail ; public : Tuple () = default ; explicit Tuple ( const Head & head , const Tail & ... tail ) : head { head }, tail { tail ...} {}// ... };

Las plantillas variádicas también pueden aplicarse a funciones, proporcionando así no solo un complemento seguro en cuanto a tipos para funciones variádicas (como printf() ), sino también permitiendo que una función llamada con printfuna sintaxis similar procese objetos no triviales.

usando std :: string_view ;plantilla < typename ... Args > void myPrintf ( string_view fmt , Args ... parameters );

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. Mediante este paquete, el usuario puede vincular cero o más argumentos a los parámetros de plantilla variádicos. Los paquetes de parámetros también pueden utilizarse 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 plantilla o de llamada a función , desempaqueta los paquetes de parámetros en argumentos separados, como en el cuerpo del siguiente ejemplo. 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 subsiguiente desempaquetado del paquete de argumentos, con las expresiones separadas por comas.args...printf()

El combinador de punto fijo a menudo se implementa utilizando argumentos de plantilla variádicos (tenga en cuenta que es equivalente a ):[](auto&&...args)[]<template...Args>(Args&&...args)

auto fix = []( auto f ) { return [ f ]( auto && ... args ) -> decltype ( auto ) { return f ( f , std :: forward < decltype ( args ) > ( args )...); }; };auto factorial = fix ([]( auto self , long n ) -> long { return n == 0 ? 1 : n * self ( self , n - 1 ); });std :: println ( "5! = {}" , factorial ( 5 )); // imprime 120

Para restringir el parámetro a ser solo de tipo T(similar a T... args) en Java, se puede usar el concepto std::same_as<T, U>[ 3 ] o std::convertible_to<From, To>(para tipos que pueden estar destinados a ser convertibles). [ 4 ]

usando std :: same_as ;// Equivalente a la declaración de Java // <T> void fn(T... args) template < typename T > void fn ( same_as < T > auto ... args ) { // ... }

Esto puede restringirse aún más a cualquier tipo específico:

using std :: convertible_to ; using std :: same_as ; using std :: string ;// Equivalente a la declaración de Java // void foo(int... args) void foo ( same_as < int > auto ... args ) { // ... }// Equivalente a la declaración de Java // void bar(String... args) void bar ( convertible_to < string > auto ... args ) { // ... }

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 similar a un printf()reemplazo variádico en C++11 sería el siguiente:

usando std :: runtime_error ;// caso base void myPrintf ( const char s []) { while ( * s ) { if ( * s == '%' ) { if ( * ( s + 1 ) == '%' ) { ++ s ; } else { throw runtime_error ( "Cadena de formato no válida: faltan argumentos" ); } } std :: println ( "{}" , * s ++ ); } }// plantilla recursiva < typename T , typename ... Args > void myPrintf ( const char s [], T value , Args ... args ) { while ( * s ) { if ( * s == '%' ) { if ( * ( s + 1 ) != '%' ) { // simula analizar el formato: solo funciona con cadenas de formato de 2 caracteres ( %d, %f, etc ); falla con %5.4f s += 2 ; // imprime el valor std :: println ( "{}" , value ); // se llama incluso cuando *s es 0 pero no hace nada en ese caso (e ignora los argumentos adicionales) myPrintf ( s , args ...); return ; } ++ s ; } std :: println ( "{}" , * s ++ ); } }

Esta es una plantilla recursiva. Observe que la versión de plantilla variádica myPrintf()se llama a sí misma, o (en caso de que args...esté vacía) llama al caso base.

No existe un mecanismo sencillo para iterar sobre los valores de la plantilla variádica. Sin embargo, hay varias maneras de traducir el paquete de argumentos en un único argumento que se pueda evaluar por separado para cada parámetro. Normalmente, esto se basará en la sobrecarga de funciones o, si la función puede simplemente tomar un argumento a la vez, en el uso de un marcador de expansión simple:

plantilla < typename ... Args > inline void pass ( Args && ... args ) { // ... }

que se puede utilizar de la siguiente manera:

plantilla < typename ... Args > inline void expand ( Args && ... args ) { pass ( foo ( args )...); }expandir ( 42 , "respuesta" , verdadero );

que se expandirá a algo como:

pasar ( foo ( arg1 ), foo ( arg2 ), foo ( arg3 ) /* etc... */ );

El uso de esta función "pass" es necesario, ya que la expansión del paquete de argumentos procede separando los argumentos de la llamada a la función por comas, que no son equivalentes al operador coma . Por lo tanto, nunca funcionará. Además, la solución anterior solo funcionará cuando el tipo de retorno de no sea . Además, las 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 usar listas de inicialización encerradas entre llaves, que garantizan un orden estricto de evaluación de izquierda a derecha. Una lista de inicialización requiere un tipo de retorno que no sea , pero se puede usar el operador coma para generar para cada elemento de expansión.foo(args)...;foo()voidfoo()void1

struct Pass { template < typename ... T > explicit Pass ( T ... args ) { // ... } };Pasar {( foo ( args ), 1 )...};

En lugar de ejecutar una función, se puede especificar una expresión lambda y ejecutarla directamente, lo que permite ejecutar secuencias arbitrarias de instrucciones en el mismo lugar.

Pass {([ & ]() -> void { std :: println ( "{}" , args ); }(), 1 )...};

Sin embargo, en este ejemplo en particular, no es necesaria una función lambda. En su lugar, se puede utilizar una expresión más común:

Pasar {( std :: println ( "{}" , args ), 1 )...};

En C++17 , estas se pueden reescribir utilizando expresiones de plegado en el operador coma:

([ & ]() -> void { std :: println ( "{}" , args ); }(), ...); (( std :: println ( "{}" , args )), ...);

Otra forma es usar la sobrecarga con "versiones de terminación" de las 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, ya que un paquete de parámetros variádico por sí solo no puede desambiguar una llamada). Por ejemplo:

void foo () { // versión de terminación }template < typename First , typename ... Args > void foo ( const First & first , const Args && ... args ) { process ( first ); foo ( args ...); // nota: ¡first no aparece aquí! }

Si args...contiene al menos un argumento, redirigirá a la segunda versión; un paquete de parámetros puede estar vacío, en cuyo caso simplemente redirigirá a la versión de terminación, que no hará nada.

Las plantillas variádicas también se pueden usar en una especificación de excepción, una lista de clase base o la lista de inicialización de un constructor. Por ejemplo, una clase puede especificar lo siguiente:

plantilla < typename ... Bases > clase Foo : público Bases ... { público : explícito Foo ( Bases && ... bases ) : Bases ( bases )... {} };

El operador de desempaquetado replicará los tipos de las clases base de Foo, 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 inicializar las clases base de Foo.

En lo que respecta a las plantillas de funciones, los parámetros variádicos se pueden reenviar. Cuando se combina con referencias universales (véase más arriba), esto permite un reenvío perfecto, como en este ejemplo de puntero inteligente :

plantilla < typename T > usando SharedPtr = std :: shared_ptr < T > ;plantilla < typename T > struct SharedPtrAllocator { plantilla < typename ... Args > SharedPtr < T > construct ( Args && ... params ) { return SharedPtr < T > ( new T ( std :: forward < Args > ( params )...)); } };

Esto desempaqueta la lista de argumentos en el constructor de T. La sintaxis reenvía perfectamente los argumentos como sus tipos correctos, incluso en lo que respecta a la naturaleza rvalue, al constructor. El operador de desempaquetado 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 para mayor seguridad con respecto a las fugas de memoria.std::forward<Args...>(params)std::shared_ptr<T>

Además, el número de argumentos en un paquete de parámetros de plantilla se puede determinar de la siguiente manera:

plantilla < typename ... Args > struct SizeCarrier { static constexpr size_t SIZE = sizeof ...( Args ); };

La expresión producirá , mientras que dará .SizeCarrier<T,U>::SIZE2SizeCarrier<>::SIZE0

amigos variádicos

En C++26 , se añadieron al lenguaje funciones amigas variádicas, que se basan en plantillas variádicas.

importar std ;plantilla < typename ... Friends > clase WithFriends { privado : int secretValue ;amigo Amigos ...; público : explícito ConAmigos ( int secreto ) : valorSecreto { secreto } {} };clase A { public : void readSecret ( const class WithFriends < A , B >& instance ) const { std :: println ( "Inspeccionando el valor secreto de A: {}" , instance.secretValue ) ; } };clase B { public : void readSecret ( const class WithFriends < A , B >& instance ) const { std :: println ( "Inspeccionando el valor secreto de B: {}" , instance.secretValue ) ; } };int main ( int argc , char * argv [ ] ) { WithFriends < A , B > secretHolder ( 135 ); A a ; B b ; a.readSecret ( secretHolder ) ; b.readSecret ( secretHolder ) ; }

Indexación de paquetes

La indexación de paquetes se introdujo en C++ en C++26, que recupera un tipo o valor en un índice específico de un paquete con una sintaxis menos recursiva, mientras que el índice debe ser una expresión constante. [ 5 ] La sintaxis de la indexación de paquetes es .id-expression...[expression]

importar std ;using std :: index_sequence ; using std :: index_sequence_for ; using std :: tuple ;// Esta función, reversed(), toma cualquier número de argumentos y devuelve una // tupla<Ts...> que contiene los mismos argumentos en orden inverso con cada // T en Ts... decaído como decay_t<T>. template < typename ... Ts > requires ( sizeof ...( Ts ) > 0 ) constexpr auto reversed ( Ts && ... args ) noexcept { constexpr size_t MAX_INDEX_SIZE = sizeof ...( args ) - 1 ; auto reverser = [ & args ...] < size_t ... Is > ( index_sequence < Is ... > ) { auto reverseForOneIndex = [ & args ...] < size_t I > { return args ...[ MAX_INDEX_SIZE - I ]; }; return std :: make_tuple ( reverseForOneIndex . template operator () < Is > ()...); }; return reverser ( index_sequence_for < Ts ... > {}); }int main () { // tipo de resultado: // tuple<char, long double, double, float, const char*, int> constexpr tuple result = reversed ( 13 , "hello" , 2.718f , 3.14 , 9.9375L , 'X' ); static_assert ( std :: get < 0 > ( result ) == 'X' ); }

Iterar sobre paquetes de tipos y parámetros

C++26 agrega sentencias de expansión ( bucles), declaradas con para iterar sobre , tipos de colección como y , y paquetes de tipo/parámetro ( ). [ 6 ]templatefortemplate for (init_stmtopt; for-range-declaration : expansion-initializer) compound-stmtstd::tuple<Ts...>std::array<T, N>std::vector<T>Ts...args

importar std ;void printAllObjects ( auto && ... args ) { template for ( auto && arg : { args ...}) { std :: print ( "{}" , arg ); } }int main () { printAllObjects ( "13 + 31 = " , 44 , " \n ¡Hola, mundo! \n " , 3.14 , '\n' , 2.718f , '\n' , 735.9572L , false ); return 0 ; }

Las instrucciones de expansión permiten breaky continuecontrolan las instrucciones de flujo, que se comportan igual que en cualquier otro bucle. [ 6 ]

importar std ;using std :: index_sequence ; using std :: index_sequence_for ;template < typename ... Ts > constexpr bool areEvenIndicesEven ( Ts && ... args ) noexcept { auto checkEvenIndices = [ & args ...] < size_t ... Is > ([[ maybe_unused ]] index_sequence < Is ... > s ) -> bool { template for ( constexpr size_t I : { Is ...}) { if constexpr ( I % 2 != 0 ) { continue ; } if ( args ...[ I ] % 2 != 0 ) { return false ; } } return true ; }; return checkEvenIndices ( index_sequence_for < Ts ... > {}); }int main () { static_assert ( areEvenIndicesEven ( 0 , 1 , 2 , 3 )); }

Carbón

Carbon , un lenguaje diseñado teniendo en cuenta la interoperabilidad con C++, ofrece variádicas, en particular expansiones de paquetes. [ 7 ]

// Toma un número arbitrario de vectores con tipos de elementos arbitrarios y // devuelve un vector de tuplas donde el i-ésimo elemento del vector es // una tupla de los i-ésimos elementos de los vectores de entrada. fn Zip [ ... each ElementType :! type ]( ... each vector : Vector ( each ElementType )) -> Vector (( ... each ElementType )) { ... var each iter : auto = each vector . Begin (); var result : Vector (( ... each ElementType )); while ( ... and each iter != each vector . End ()) { result . push_back (( ... each iter )); ... each iter ++ ; } return result ; }

D

Definición

La definición de plantillas variádicas en D es similar a su equivalente en C++:

plantilla VariadicTemplate ( Args ...) { // cuerpo aquí... }

Asimismo, cualquier argumento puede preceder a la lista de argumentos:

plantilla VariadicTemplate ( T , string value , alias symbol , Args ...) { // cuerpo aquí... }

Uso básico

Los argumentos variádicos son muy similares a los arreglos constantes en su uso. Se pueden iterar, acceder a ellos mediante un índice, tienen una lengthpropiedad y se pueden segmentar . 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 una función).

Cualquier cosa que se conozca en tiempo de compilación se puede pasar como argumentos variádicos. 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...).

Aquí hay un ejemplo que imprime la representación en cadena de los parámetros variables StringOfy StringOf2produce resultados iguales.

entero estático entero estático ;struct Dummy {}void main () { pragma ( msg , StringOf !( "Hola mundo" , uint , Dummy , 42 , staticInt )); pragma ( msg , StringOf2 !( "Hola mundo" , uint , Dummy , 42 , staticInt )); }plantilla StringOf ( Args ...) { enum StringOf = Args [ 0 ]. stringof ~ StringOf !( Args [ 1. .$]); }plantilla StringOf () { enum StringOf = "" ; }plantilla StringOf2 ( Args ...) { static if ( Args . length == 0 ) { enum StringOf2 = "" ; } else { enum StringOf2 = Args [ 0 ]. stringof ~ StringOf2 !( Args [ 1. .$]); } }

Salidas:

"Hola mundo"uintDummy42staticInt "Hola mundo"uintDummy42staticInt 

AliasSeq

Las plantillas variádicas se utilizan a menudo para crear una secuencia de alias, denominada std.meta.AliasSeq . La definición de un alias es en realidad muy sencilla:std.meta.AliasSeq

alias AliasSeq ( Args ...) = Args ;

Esta estructura permite manipular una lista de argumentos variables que se expandirán automáticamente. Los argumentos deben ser símbolos o valores conocidos en tiempo de compilación. Esto incluye valores, tipos, funciones o incluso plantillas no especializadas. Esto permite cualquier operación que cabría esperar:

import std.meta ;void main () { // Nota: AliasSeq no se puede modificar, y un alias no se puede reasignar, por lo que necesitaremos definir nuevos nombres para nuestras modificaciones. alias numbers = AliasSeq !( 1 , 2 , 3 , 4 , 5 , 6 ); // Segmentación alias lastHalf = numbers [$ / 2 .. $]; static assert ( lastHalf == AliasSeq !( 4 , 5 , 6 )); // Auto expansión 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 number ) { enum isEven = ( 0 == ( number % 2 )); }

En C# / .NET , la clase System.Tupletiene instanciaciones específicas para 1 a 8 elementos. Para crear una tupla con nueve o más componentes, el último parámetro TRestse Tuple<T1, T2, T3, T4, T5, T6, T7, TRest>proporciona como otra tupla, ya que C# carece de parámetros de plantilla variádicos. [ 8 ]

En Java , se pueden emular parámetros variádicos usando el tipo superior java.lang.Object(de manera similar en C#/.NET, con System.Object), sin embargo, esto carece de la seguridad de tipos de los parámetros variádicos al estilo C++.

import java.util.List ;void printAllObjects ( Object ... args ) { for ( Object arg : args ) { System . out . println ( arg ); } }void main () { printAllObjects ( "Hola, mundo!" , 42 , 3.14159f , true , List . of ( 1 , 2 , 3 )); }

En Rust existen propuestas para genéricos variádicos desde 2013 [ 9 ] , pero aún no se han incorporado al lenguaje. Sin embargo, se pueden emular de forma similar mediante macros variádicas , que pueden utilizarse para la generación de código [ 10 ] .

Véase también

Para artículos sobre construcciones variádicas distintas de las plantillas

Referencias

  1. Douglas Gregor y Jaakko Järvi (febrero de 2008). "Plantillas variádicas para C++0x" . Journal of Object Technology . 7 (2, Número especial OOPS Track en SAC 2007): 31–51 . doi : 10.5381/jot.2008.7.2.a2 .
  2. Douglas Gregor; Jaakko Järvi y Gary Powell. (Febrero de 2004). "Plantillas variádicas. Número N1603=04-0043 en el correo previo a Sídney del Comité de Estándares ISO C++".
  3. cppreference.com. "std::same_as" . cppreference.com . cppreference.com . Consultado el 17 de mayo de 2026 .
  4. cppreference.com. "std::convertible_to" . cppreference.com . cppreference.com . Consultado el 17 de mayo de 2026 .
  5. "Indexación de paquetes (desde C++26)" . cppreference.com . 16 de abril de 2025. Consultado el 29 de marzo de 2026 .
  6. 1 2 Dan Katz (20 de junio de 2025). "Declaraciones de expansión" . open-std.org . WG21.
  7. Proyecto de lenguaje Carbono (1 de abril de 2026). "Variadics - Documentación del lenguaje Carbono" . docs.carbon-lang.dev . Proyecto de lenguaje Carbono.
  8. "Clase Tuple<T1, T2, T3, T4, T5, T6, T7, TRest>" . learn.microsoft.com . Microsoft Learn . Consultado el 14 de octubre de 2025 .
  9. El equipo de Rust (12 de julio de 2023). "Genéricos variádicos" . github.com . El equipo de Rust.
  10. El equipo de Rust (1 de abril de 2026). "Interfaces variádicas" . rust-lang.org . Rust por ejemplo.
  • Borrador de trabajo para el lenguaje C++, 16 de enero de 2012
  • Plantillas variádicas en el lenguaje D