¿Cómo se recorre un paquete de parámetros usando una expansión de paquete?
Estoy intentando aprender plantillas y funciones variadas. No puedo entender por qué este código no se compila:
template<typename T>
static void bar(T t) {}
template<typename... Args>
static void foo2(Args... args)
{
(bar(args)...);
}
int main()
{
foo2(1, 2, 3, "3");
return 0;
}
Cuando compilo falla con el error:
Error C3520: 'args': el paquete de parámetros debe expandirse en este contexto
(en función foo2
).
Uno de los lugares donde puede ocurrir una expansión de paquete es dentro de una lista de inicio entre llaves . Puedes aprovechar esto colocando la expansión dentro de la lista de inicializadores de una matriz ficticia:
template<typename... Args>
static void foo2(Args &&... args)
{
int dummy[] = { 0, ( (void) bar(std::forward<Args>(args)), 0) ... };
}
Para explicar el contenido del inicializador con más detalle:
{ 0, ( (void) bar(std::forward<Args>(args)), 0) ... };
│ │ │ │ │
│ │ │ │ └─// pack expand the whole thing
│ │ │ │
│ │ └─// perfect forwarding └─// comma operator
│ │
│ └─// cast to void to ensure that regardless of bar()'s return type
│ // the built-in comma operator is used rather than an overloaded one
│
└─// ensure that the array has at least one element so that we don't try to make an
// illegal 0-length array when args is empty
Demostración .
Una ventaja importante de expandirse {}
es que garantiza una evaluación de izquierda a derecha.
Con expresiones plegables de C++ 17 , puedes simplemente escribir
((void) bar(std::forward<Args>(args)), ...);
Los paquetes de parámetros solo se pueden expandir en una lista de contextos estrictamente definida y el operador ,
no es uno de ellos. En otras palabras, no es posible utilizar la expansión del paquete para generar una expresión que consta de una serie de subexpresiones delimitadas por el operador ,
.
La regla general es "La expansión puede generar una lista de ,
patrones separados donde ,
hay un delimitador de lista ". El operador ,
no construye una lista en el sentido gramatical.
Para llamar a una función para cada argumento, puede usar la recursividad (que es la herramienta principal en el cuadro del programador de plantillas variadas):
template <typename T>
void bar(T t) {}
void foo2() {}
template <typename Car, typename... Cdr>
void foo2(Car car, Cdr... cdr)
{
bar(car);
foo2(cdr...);
}
int main()
{
foo2 (1, 2, 3, "3");
}
Ejemplo en vivo
COPIA DESCARADA [aprobada por su fuente]
Los paquetes de parámetros solo se pueden expandir en una lista de contextos estrictamente definida y el operador ,
no es uno de ellos. En otras palabras, no es posible utilizar la expansión del paquete para generar una expresión que consta de una serie de subexpresiones delimitadas por el operador ,
.
La regla general es "La expansión puede generar una lista de ,
patrones separados donde ,
hay un delimitador de lista". El operador ,
no construye una lista en el sentido gramatical.
Para llamar a una función para cada argumento, puede usar la recursividad (que es la herramienta principal en el cuadro del programador de plantillas variadas):
#include <utility>
template<typename T>
void foo(T &&t){}
template<typename Arg0, typename Arg1, typename ... Args>
void foo(Arg0 &&arg0, Arg1 &&arg1, Args &&... args){
foo(std::forward<Arg0>(arg0));
foo(std::forward<Arg1>(arg1), std::forward<Args>(args)...);
}
auto main() -> int{
foo(1, 2, 3, "3");
}
INFORMACIÓN ÚTIL NO COPIA
Otra cosa que probablemente no hayas visto en esta respuesta es el uso del &&
especificador y std::forward
. En C++, el &&
especificador puede significar una de dos cosas: rvalue-references o referencias universales.
No entraré en referencias de valores, sino con alguien que trabaje con plantillas variadas; Las referencias universales son un regalo de Dios.
Reenvío perfecto
Uno de los usos std::forward
y referencias universales es el reenvío perfecto de tipos a otras funciones.
En su ejemplo, si pasamos un int&
se foo2
degradará automáticamente int
debido a la firma de la foo2
función generada después de la deducción de la plantilla y si desea reenviarlo arg
a otra función que lo modifique por referencia, obtendrá resultados no deseados ( la variable no se cambiará) porque foo2
se pasará una referencia al temporal creado al pasarle un int
. Para solucionar esto, especificamos una función de reenvío para tomar cualquier tipo de referencia a una variable (rvalue o lvalue). Luego, para asegurarnos de que pasamos el tipo exacto pasado en la función de reenvío que usamos std::forward
, entonces y solo entonces permitimos la degradación de tipos; porque ahora estamos en el punto donde más importa.
Si lo necesitas, lee más sobre referencias universales y reenvío perfecto ; Scott Meyers es un gran recurso.
La solución C++17 para esto está muy cerca del código esperado:
template<typename T>
static void bar(T t) {}
template<typename... Args>
static void foo2(Args... args) {
(bar(args), ...);
}
int main() {
foo2(1, 2, 3, "3");
return 0;
}
Esto expande el patrón con el operador de coma entre cada expresión.
// imaginary expanded expression
(bar(1), bar(2), bar(3), bar("3"));