Ir al contenido

Variadic Templates y Fold Expressions

Maneja cualquier numero de argumentos de template con parameter packs. Domina las fold expressions para procesamiento de packs elegante y sin recursion.

Parameter Packs

Un variadic template acepta un numero arbitrario de argumentos de template usando un parameter pack. La sintaxis usa puntos suspensivos (...). Hay dos tipos de packs:

1. Template parameter pack — template — captura cero o mas argumentos de tipo.
2. Function parameter pack — void f(Ts... args) — captura los argumentos de funcion correspondientes.

No puedes iterar directamente sobre un pack en un bucle. En su lugar, lo expandes usando el operador .... La idea clave es que la expansion de pack aplica un patron a cada elemento del pack y produce una lista separada por comas.

sizeof...(Ts) y sizeof...(args) devuelven el numero de elementos en el pack como un constexpr std::size_t.

Expansion Recursiva de Pack (Patron Legacy)

Antes de las fold expressions de C++17, la forma estandar de procesar packs era la instanciacion recursiva: extraer el primer elemento, procesarlo y recurrir sobre el resto. Esto requiere un caso base para terminar la recursion.

recursive_variadic.cpp
#include <iostream>

// Base case: no arguments
void print() {
    std::cout << '\n';
}

// Recursive case: process first, recurse on rest
template <typename First, typename... Rest>
void print(const First& first, const Rest&... rest) {
    std::cout << first;
    if constexpr (sizeof...(rest) > 0) {
        std::cout << ", ";
    }
    print(rest...);  // expand rest, recurse
}

int main() {
    print(1, 2.5, "hello", 'A');
    // Output: 1, 2.5, hello, A

    print(); // just a newline
}

Fold Expressions (C++17)

Las fold expressions de C++17 eliminan la necesidad de templates recursivos en la mayoria de los casos. Una fold expression aplica un operador binario a todos los elementos de un parameter pack. Hay cuatro formas:

- Unary right fold: (pack op ...) se expande a e1 op (e2 op (... op eN))
- Unary left fold: (... op pack) se expande a ((e1 op e2) op ...) op eN
- Binary right fold: (pack op ... op init) — incluye un valor inicial
- Binary left fold: (init op ... op pack) — incluye un valor inicial

fold_expressions.cpp
#include <iostream>
#include <string>

// Sum all arguments — unary left fold with +
template <typename... Ts>
auto sum(Ts... args) {
    return (... + args);  // ((a1 + a2) + a3) + ...
}

// Print all arguments — binary left fold with <<
template <typename... Ts>
void print(Ts&&... args) {
    (std::cout << ... << args) << '\n';
    // expands to: ((std::cout << a1) << a2) << a3 ...
}

// Check if ALL arguments satisfy a predicate — unary left fold with &&
template <typename... Ts>
bool all_positive(Ts... args) {
    return (... && (args > 0));  // (a1>0) && (a2>0) && ...
}

// Push multiple elements to a vector — fold over comma operator
#include <vector>
template <typename T, typename... Ts>
void push_all(std::vector<T>& vec, Ts&&... args) {
    (vec.push_back(std::forward<Ts>(args)), ...);
}

int main() {
    std::cout << sum(1, 2, 3, 4, 5) << '\n'; // 15
    print("x=", 10, " y=", 20);               // x=10 y=20
    std::cout << std::boolalpha
              << all_positive(1, 2, 3)   << '\n'  // true
              << all_positive(1, -2, 3)  << '\n'; // false

    std::vector<int> v;
    push_all(v, 10, 20, 30);
    for (int x : v) std::cout << x << ' ';  // 10 20 30
    std::cout << '\n';
}

Contextos de Expansion de Pack

La expansion de pack (...) puede aparecer en muchos contextos, no solo en argumentos de llamada a funcion:

- Argumentos de funcion: f(args...) o f(transform(args)...)
- Argumentos de template: std::tuple
- Clases base: class Derived : public Bases... { };
- Listas de inicializacion: auto list = { args... };
- Capturas en lambda: [args...] { } o [...args = std::move(args)] { } (init-capture de C++20)
- Declaraciones using (C++17): using Bases::operator()...;

El patron es lo que aparece antes del .... En f(transform(args)...), el patron es transform(args), que se expande a f(transform(a1), transform(a2), transform(a3)).

Esto es crucial de entender: f(args)... no es lo mismo que f(args...). El primero se expande a f(a1), f(a2), f(a3) (tres llamadas separadas en una expresion de coma), mientras que el segundo se expande a f(a1, a2, a3) (una llamada con tres argumentos).

std::tuple y std::apply

std::tuple es el contenedor heterogeneo de la biblioteca estandar, construido sobre variadic templates. std::apply (C++17) llama a un invocable con los elementos de la tuple desempaquetados como argumentos. std::index_sequence permite la indexacion en tiempo de compilacion en tuples.

tuple_apply.cpp
#include <tuple>
#include <iostream>
#include <string>
#include <utility>

// std::apply calls a function with tuple elements as arguments
void greet(const std::string& name, int age) {
    std::cout << "Hello " << name << ", age " << age << '\n';
}

// Print all tuple elements using index_sequence
template <typename Tuple, std::size_t... Is>
void print_tuple_impl(const Tuple& t, std::index_sequence<Is...>) {
    ((std::cout << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
}

template <typename... Ts>
void print_tuple(const std::tuple<Ts...>& t) {
    std::cout << '(';
    print_tuple_impl(t, std::index_sequence_for<Ts...>{});
    std::cout << ")\n";
}

int main() {
    auto person = std::make_tuple(std::string("Alice"), 30);
    std::apply(greet, person);  // Hello Alice, age 30

    auto data = std::make_tuple(1, 3.14, std::string("hello"));
    print_tuple(data);  // (1, 3.14, hello)
}
Error común

Los errores mas comunes con variadic templates:

1. Olvidar el ... — escribir args cuando quieres decir args... deja el pack sin expandir, causando un error de compilacion.
2. Expandir en el lugar equivocado — f(g(args)...) vs f(g(args...)) tienen significados muy diferentes. El primero llama a g una vez por elemento; el segundo llama a g con todos los elementos.
3. Packs vacios — (... + args) con un pack vacio es mal formado para la mayoria de los operadores. Usa un binary fold con un valor inicial: (0 + ... + args).
4. sizeof... devuelve la cantidad, no el tamano en bytes — sizeof...(args) da el numero de elementos del pack, no su tamano combinado en memoria.

Buena práctica

Prefiere fold expressions sobre variadic templates recursivos siempre que sea posible. Son mas concisas, mas faciles de leer, y a menudo compilan mas rapido. Usa binary folds con un valor inicial explicito para manejar packs vacios de forma segura. Reserva la expansion recursiva para casos donde las fold expressions son insuficientes — por ejemplo, cuando necesitas procesar elementos en pares, o cuando el procesamiento de cada elemento requiere logica diferente.

Puntos clave
  • Los variadic templates usan typename... Ts (template parameter pack) y Ts... args (function parameter pack)
  • sizeof...(args) devuelve el numero de elementos en un pack en tiempo de compilacion
  • La expansion de pack aplica un patron a cada elemento: f(transform(args)...) llama a transform sobre cada elemento
  • Las fold expressions (C++17) reemplazan la expansion recursiva: (... + args) suma todos los elementos
  • Usa binary folds con un valor inicial para el manejo seguro de packs vacios
  • std::apply desempaqueta una tuple como argumentos de funcion; std::index_sequence permite la indexacion en tiempo de compilacion

Quiz — Pon a prueba tus conocimientos

(20 XP)

1. A que se expande `(... + args)` para `args = {a, b, c}`?

2. Cual es la diferencia entre `f(args...)` y `f(args)...`?

3. Que devuelve `sizeof...(args)`?