Puntos suspensivos y plantillas variádicas

En este artículo se muestra cómo utilizar los puntos suspensivos (...) con plantillas variádicas de C++. Los puntos suspensivos han tenido muchos usos en C y C++. Entre ellos se incluyen listas de argumentos de variable para funciones. La función printf() de la biblioteca en tiempo de ejecución de C es uno de los ejemplos más conocidos.

Una plantilla variádica es una plantilla de clase o de función que admite un número arbitrario de argumentos. Este mecanismo es especialmente útil para los desarrolladores de bibliotecas de C++: puede aplicarlo a plantillas de clase y plantillas de función y, por tanto, proporcionar una amplia gama de funcionalidades y flexibilidad no triviales y seguras para tipos.

Sintaxis

Las plantillas variádicas utilizan los puntos suspensivos de dos maneras. A la izquierda del nombre de parámetro, significa un paquete de parámetros, y a la derecha del nombre de parámetro, expande los paquetes de parámetros en nombres diferentes.

Este es un ejemplo básico de sintaxis de definición de plantilla de clase variadic:

template<typename... Arguments> class classname;

En el caso de los paquetes de parámetros y las expansiones, puede agregar espacios en blanco alrededor de los puntos suspensivos, según sus preferencias, como se muestra en este ejemplo:

template<typename ...Arguments> class classname;

O este ejemplo:

template<typename ... Arguments> class classname;

En este artículo se usa la convención que se muestra en el primer ejemplo (los puntos suspensivos se adjuntan a typename).

En los ejemplos anteriores, Arguments es un paquete de parámetros. La clase classname puede aceptar un número variable de argumentos, como en estos ejemplos:

template<typename... Arguments> class vtclass;

vtclass< > vtinstance1;
vtclass<int> vtinstance2;
vtclass<float, bool> vtinstance3;
vtclass<long, std::vector<int>, std::string> vtinstance4;

Mediante el uso de una definición de plantilla de clase variádica, también puede requerir al menos un parámetro:

template <typename First, typename... Rest> class classname;

Este es un ejemplo básico de sintaxis de plantilla de función variadic:

template <typename... Arguments> returntype functionname(Arguments... args);

A Arguments continuación, el paquete de parámetros se expande para su uso, como se muestra en la sección siguiente.

Otras formas de sintaxis de plantilla de función variadic son posibles, incluidos, entre otros, estos ejemplos:

template <typename... Arguments> returntype functionname(Arguments&... args);
template <typename... Arguments> returntype functionname(Arguments&&... args);
template <typename... Arguments> returntype functionname(Arguments*... args);

También se permiten especificadores como const:

template <typename... Arguments> returntype functionname(const Arguments&... args);

Como ocurre con las definiciones de clase de plantilla variádica, se pueden crear funciones que requieran al menos un parámetro:

template <typename First, typename... Rest> returntype functionname(const First& first, const Rest&... args);

Las plantillas variádicas utilizan el operador sizeof...() (relacionado con el anterior operador sizeof()):

template<typename... Arguments>
void tfunc(const Arguments&... args)
{
    constexpr auto numargs{ sizeof...(Arguments) };

    X xobj[numargs]; // array of some previously defined type X

    helper_func(xobj, args...);
}

Más información sobre la posición de los puntos suspensivos

Anteriormente, en este artículo se describe la colocación de puntos suspensivos que define paquetes de parámetros y expansiones en este formato: "a la izquierda del nombre del parámetro, significa un paquete de parámetros y, a la derecha del nombre del parámetro, expande los paquetes de parámetros en nombres independientes". Aunque técnicamente es cierto, puede resultar confuso en la traducción al código. Tenga en cuenta lo siguiente:

  • En una lista de parámetros de plantilla (template <parameter-list>), typename... introduce un paquete de parámetros de plantilla.

  • En una cláusula de declaración de parámetros (func(parameter-list)), los puntos suspensivos "de nivel superior" introducen un paquete de parámetros de función y la posición de los puntos suspensivos es importante:

    // v1 is NOT a function parameter pack:
    template <typename... Types> void func1(std::vector<Types...> v1);
    
    // v2 IS a function parameter pack:
    template <typename... Types> void func2(std::vector<Types>... v2);
    
  • Cuando los puntos suspensivos aparecen inmediatamente después de un nombre de parámetro, tiene una expansión del paquete de parámetros.

Ejemplo

Una buena manera de ilustrar el mecanismo de plantilla de función variadic es usarlo en una reescritura de algunas de las funciones de printf:

#include <iostream>

using namespace std;

void print() {
    cout << endl;
}

template <typename T> void print(const T& t) {
    cout << t << endl;
}

template <typename First, typename... Rest> void print(const First& first, const Rest&... rest) {
    cout << first << ", ";
    print(rest...); // recursive call using pack expansion syntax
}

int main()
{
    print(); // calls first overload, outputting only a newline
    print(1); // calls second overload

    // these call the third overload, the variadic template,
    // which uses recursion as needed.
    print(10, 20);
    print(100, 200, 300);
    print("first", 2, "third", 3.14159);
}

Salida

1
10, 20
100, 200, 300
first, 2, third, 3.14159

Nota:

La mayoría de las implementaciones que incorporan plantillas de función variadic usan recursividad de algún tipo, pero es ligeramente diferente de la recursividad tradicional. La recursión tradicional implica que una función se llame a sí misma al usar la misma firma. (Puede estar sobrecargado o ser una plantilla, pero se elige la misma firma cada vez). La recursividad variádica consiste en llamar a una plantilla de función variádica mediante el uso de números diferentes de argumentos (casi siempre decrecientes) y así imprimir una firma diferente cada vez. Sigue siendo necesario un "caso base", pero la naturaleza de la recursividad es diferente.